0%

[UnionCTF2021]Cr0wnAir

[UnionCTF2021]Cr0wnAir

最近天天被外国比赛暴打。。。。这次UnionCTF两个web,一个sqlite的union注入,零过滤,百度的payload都能打通,还有就是这个题,还蛮有意思的,后面这步利用感觉还比较实用,学习了

源码

贴一部分

checkin.js
const pattern = {
  firstName: /^\w{1,30}$/,
  lastName: /^\w{1,30}$/,
  passport: /^[0-9]{9}$/,
  ffp: /^(|CA[0-9]{8})$/,
  extras: [
    {sssr: /^(BULK|UMNR|VGML)$/},
  ],
};
function isSpecialCustomer(passport, frequentFlyerNumber) {
  return false;
}

function createToken(passport, frequentFlyerNumber) {
  var status = isSpecialCustomer(passport, frequentFlyerNumber) ? "gold" : "bronze";
  var body = {"status": status, "ffp": frequentFlyerNumber};
  return jwt.encode(body, config.privkey, 'RS256');
}
.....
  if (jpv.validate(data, pattern, { debug: true, mode: "strict" })) {
    if (data["firstName"] == "Tony" && data["lastName"] == "Abbott") {
      var response = {msg: "You have successfully checked in! Please remember not to post your boarding pass on social media."};
    } else if (data["ffp"]) {
      var response = {msg: "You have successfully checked in. Thank you for being a Cr0wnAir frequent flyer."};
      for(e in data["extras"]) {
        if (data["extras"][e]["sssr"] && data["extras"][e]["sssr"] === "FQTU") {
          var token = createToken(data["passport"], data["ffp"]);
          var response = {msg: "You have successfully checked in. Thank you for being a Cr0wnAir frequent flyer. Your loyalty has been rewarded and you have been marked for an upgrade, please visit the upgrades portal.", "token": token};
        }
      }

简而言之,就是用了一个jpv库进行正则匹配,如果提交的数据能过匹配然后就根据情况回显,如果extras下的sssr属性有一项为FQTU就签一个RS256的青铜票据下来,但是显然之前的正则是不允许提交这么个FQTU的

获取了token之后就可以去upgrades路由换flag,如下

upgrades.js
function getLoyaltyStatus(req, res, next) {
  if (req.headers.authorization) {
    let token = req.headers.authorization.split(" ")[1];
    try {
      var decoded = jwt.decode(token, config.pubkey);
    } catch {
      return res.json({ msg: 'Token is not valid.' });
    }
    res.locals.token = decoded;
  }
  next()
}
.....
router.post('/flag', [getLoyaltyStatus], function(req, res, next) {
  if (res.locals.token && res.locals.token.status == "gold") {
    var response = {msg: config.flag };
  } else {
    var response = {msg: "You do not qualify for this upgrade at this time. Please fly with us more."};
  }
  res.json(response);
});

解码jwt查看票据类型,黄金票据就给flag,使用npm aduit可以看到这个jwt的版本较老,存在一个严重的漏洞,即jwt.decode未指定算法时,可以通过jwt中携带的算法进行验证

题解

可以很清楚的看出来解题思路,1.绕过正则获取一个青铜票据,2.通过某种方法从签发的token中还原出公钥,然后将jwt验证算法更改为对称算法HS256,则可以直接用公钥对称加解密完成cookie伪造

正则绕过

jpv查了一下是一个比较小众的库,直接在GitHub上就能看到一个似乎有用的issue
A vulnerability in validate()
jpv的版本也较老,符合利用要求,该版本之前jpv在比较时有如下语句

if (typeof pattern === 'object') { 
     if (typeof pattern !== typeof value) { 
         return res(false) 
     } 
     if (pattern !== null && value !== null) { 
         return res(value.constructor.name === pattern.constructor.name) 
     } 
     return res(value === pattern) 
 } 

当模式是一个对象,且双方均不为null时,将返回value的constructor属性的name和模式的对比的结果,因此构造如下一个对象,就能进行欺骗进行绕过

var user_input = {
    should_be_arrary: {"a":1, 'constructor': {'name':'Array'}}
};
var pattern = {
    should_be_arrary: []
};

但是这里有一个很奇怪的地方,jpv验证使用了mode: "strict"这个模式只允许值与模式的数据完全一致,不允许存在多的键值对,应该额外构造出来的属性是不能过这个strict条件的?

公钥还原

说实话这步真超出认知范围
我一开始想的就是暴力破解。。。但是直接遍历硬跑肯定没机会,数学不好的我一度认为可能能构造一对密钥使得公钥很短私钥很长。。。。能让我暴力破解
然后老国王告诉我这个RSA1024的公私钥都至尊长,直接放弃,不过很令人吃惊的是无敌的非对称加密算法居然允许从密文中推导出公钥!

At this point it’s important to remember that although public key cryptosystems guarantee that the private key can’t be derived from the public key, signatures, ciphertexts, etc., there are usually no such guarantees for the public key!

分解公因数什么的进行超级爆破,数学不好的我已经不太记得清是怎么回事了。。。
但是工具拿来能用就行,只要两个私钥签的jwt就能推导出公钥,本地爆破了一下十分钟左右就出了(工具在用的时候cryptography版本不一样,有个函数位置变了,需要自己改一下才能用)

用公钥签一个新的cookie,签一个黄金票据,算法改为对称算法HS256,获得flag

参考链接

ABUSING JWT PUBLIC KEYS WITHOUT THE PUBLIC KEY
Cr0wnAir wp
工具