Skip to main content

函数库:str - 字符串工具集

用户在使用 str 库之前,我们推荐用户先完成 Yaklang 字符串基础语法的学习,相关链接如下

  1. 字符串:基础字符串使用
  2. 字符串:字节序列
  3. 字符串:格式化 ...

在完成字符串基本定义的学习之后,我们就可以来学习如何使用 str 函数集来帮助你的工作,接下来我们将详细介绍一些常见的 str 函数:

str.ParseStringTo... 系列:解析扫描目标#

这一系列函数可以方便用户快速处理扫描目标,包括但不限于:端口,主机,Url,CIDR C段;该系列有如下函数:

// 把字符串解析成 C段 CIDR 的 C 段形式,用逗号分割,解析结果也用逗号进行分割str.ParseStringToCClassHosts(targets string) string
// 把字符串切分成主机端口形式// 可以支持 URL 切分// Host:Port 切分等str.ParseStringToHostPort(raw string) (host string, port int, err error)
// 把主机列表解析成多台主机:支持逗号分隔,支持 CIDR 解析,支持网段范围解析(192.168.1.1-21)str.ParseStringToHosts(raw string) []string
// 把主机端口字符串解析成端口列表str.ParseStringToPorts(ports string) []int
// 把一段文本按行进行解析str.ParseStringToLines(raw string) []string
// 把主机列表解析成 URL:支持逗号分隔,支持 CIDR 解析,支持网段范围解析(192.168.1.1-21)str.ParseStringToUrls(targets ...string) []stringstr.ParseStringToUrlsWith3W(sub ...string) []string
// 把字符串或字节序列解析成 http 标准请求和响应str.ParseBytesToHTTPRequest(raw []byte) (*http.Request, error)str.ParseBytesToHTTPResponse(res []byte) (*http.Response, error)str.ParseStringToHTTPRequest(raw string) (*http.Request, error)str.ParseStringToHTTPResponse(res string) (*http.Response, error)

解析扫描目标主机#

扫描是 Yak 编写脚本的一个大的需求,因此提供合理快速的目标解析与拆分工具函数是非常必要的,通过上面的函数功能,我们可以快速实现目标的解析。

最基础地,我们可以解析一个主机列表:

input = `192.168.1.1/24,192.168.2.101`results = str.ParseStringToHosts(input)~for result in results {    println(result)}
/*Output:
192.168.1.0192.168.1.1......192.168.1.254192.168.1.255192.168.2.101*/
注意 ~ 是 Yaklang 的特殊语法

带有 ~ 结尾的函数调用将会自动忽略错误,如果错误不为空,将会造成当前程序抛出错误

详情我们可以参考如下两个章节:

  1. 函数简化调用:WavyCall
  2. 异常处理:try-catch

除此之外,用户输入往往有更复杂的情况:

  1. 192.168.1.1:80
  2. example.com
  3. https://www.example.com/abc
  4. 192.168.1.1-30
  5. example.com:80

这些情况往往都能被 str.ParseStringToHosts 很好处理(当然,多个目标的输入仍然是逗号分隔

input = `192.168.1.1/24,192.168.2.101,www.example.com,www.example.com:80,https://example.com,192.168.1.1:80,192.168.1.1-5,https://example.com/abc`results = str.ParseStringToHosts(input)~for result in results {    println(result)}
/*输出:
192.168.1.0192.168.1.1......192.168.1.254192.168.1.255192.168.2.101www.example.comwww.example.com:80https://example.com192.168.1.1:80192.168.1.1192.168.1.2192.168.1.3192.168.1.4192.168.1.5https://example.com/abc*/

解析扫描端口#

要完成端口的解析操作,我们使用的函数是 str.ParseStringToPorts 这个函数支持一个字符串输入,并能把字符串(逗号分隔端口)输入尽力解析成一个端口列表。

如下案例可以快速了解这个函数的功能。

input = `80,443,8000-8008,8080-8088`ports = str.ParseStringToPorts(input)~for port in ports {    println(port)}
/*输出:
80443800080018002...80078008808080818082...80878088*/
该函数的返回结果是 []int

需要注意的是,str.ParseStringToPorts 的返回结果是 []int,如果用户要把 int 作为 string 来使用的话,需要小心一些,不然会触发类型错误。

我们可以通过很多简单的方式把数字转变成字符串,例如

a = parseString(80); dump(a)/* OUTPUT: (string) (len=2) "80" */
a = string(80); dump(a)/* OUTPUT: (string) (len=2) "80" */
a = sprint(80); dump(a)/* OUTPUT: (string) (len=2) "80" */
port = 80a = f`${port}`; dump(a)/* OUTPUT: (string) (len=2) "80" */

切分主机与端口#

通常情况下,我们无法要求用户输入的目标是规整的,因此我们经常需要知道用户输入的到底是主机还是端口。那么能够单个主机拆分成 Host/Domain/IP:Port 的函数就变得非常重要了。

我们在 str 函数库中可以找到 str.ParseStringToHostPort 这个函数,可以很方便的完成这个小目标:

input = `https://example.com`host, port = str.ParseStringToHostPort(input)~  // host: example.com   port: 443
input = `http://example.com`host, port = str.ParseStringToHostPort(input)~  // host: example.com   port: 80
input = `http://example.com:8080`host, port = str.ParseStringToHostPort(input)~  // host: example.com   port: 8080
input = `example.com:8081`host, port = str.ParseStringToHostPort(input)~  // host: example.com   port: 8081
input = `192.168.1.1:445`host, port = str.ParseStringToHostPort(input)~  // host: 192.168.1.1   port: 445

我们可以在这个函数中,快速实现精准地对 URL 中主机和端口进行拆分,当然也能快速实现 Host:Port 或者 IP:Port 的拆分。

错误情况

如果我们输入了一个不可能推断出端口的函数,那么我们一般会获得如下内容,它会造成解析错误

input = `192.168.1.1`host, port = str.ParseStringToHostPort(input)~/*Panic Stack:File "/var/folders/0f/ypm71yhs1jdg_nrgs_8_j3180000gn/T/yaki-code-1941772628.yak", in global code--> 17 host, port = str.ParseStringToHostPort(input)~  // host: 192.168.1.1   port: 445
YakVM Panic: native func `str.ParseStringToHostPort` call error: unknown port for [192.168.1.1]*/

这个错误一般来说是可以被 try 和 catch 捕获的。例如我们可以使用如下代码捕获错误。

try {    input = `192.168.1.1`    host, port = str.ParseStringToHostPort(input)~} catch err {    dump(err)}

str.ParseStringToHostPort 的反义函数是 str.HostPort,因此我们可以使用这个函数快速组合一个扫描目标主机和端口。

// 基础使用result = str.HostPort("127.0.0.1", 8080) // result: 127.0.0.1:8080
// 把端口和主机进行两两组合for host in str.ParseStringToHosts("192.168.1.1/24")~ {    for port in str.ParseStringToPorts("8000-8080")~ {        println(str.HostPort(host, port))    }}
/*OUTPUT:
192.168.1.1:8000192.168.1.1:8001192.168.1.1:8002192.168.1.1:8003......192.168.1.255:8076192.168.1.255:8077192.168.1.255:8078192.168.1.255:8079192.168.1.255:8080*/

数据提取:#

从 HTML 中提取 Title#

在对数据包结果的处理中,我们经常需要提取 HTML 中的 Title 标签内容,这个内容一般用于指纹识别和内容分析。

str.ExtractTitle 中,我们可以直接做到这个操作,函数定义为 ExtractTitle(any) string 从任何可能的文本中提取一个 <title></title> 标签。

input = `<html>    <head>        <title>This is a title tag in HTML!</title>    </head>    <body className="hello-class">        Hello World HTML!    </body><html>`
title = str.ExtractTitle(input)/* title: (string) (len=28) "This is a title tag in HTML!" */
函数原理是正则,但是结果经过 UTF8 修正

这个函数本质上只是一个正则主导的函数:(?is)\<title\>(.*?)\</?title\>

通过这个正则来操作的。

但它经过 UTF8 修正,因此他是一个 UTF8-Safe 安全的

从任意文本提取 JSON#

这个函数非常有助于我们自己处理任何流量中的敏感信息,这是一个非常有意思的函数,可以提取数据包或者大段文本中任何 "长得像 JSON" 的数据。因此如果流量中有 gRPC 包含的 JSON 信息,或者 XML 中的 JSON 信息也会被提取出来。

我们观察如下案例

input = `HTTP/1.1 200 OKCache-Control: max-age=864000Connection: keep-aliveSet-Cookie: useData={"username": "admin"}Content-Length: 8622
<!DOCTYPE html><html>
    <div>{"abc": 111, "data": [1,2,3,4]}</div></html>`
jsons = str.ExtractJson(input)for jsonStr in jsons {    println(jsonStr)}
/*OUTPUT:
{"username": "admin"}{"abc": 111, "data": [1,2,3,4]}*/

我们使用这个函数可以把 JSON 从文本的任意位置提取出来。结果是一个 []string,因此可以通过 for 循环来实现取内容处理。

该函数实现原理:下推自动机﹙PDA﹚

本函数并不是通过正则技术实现的,是通过 PDA 技术实现。

简单来说,我们了解 JSON 的基本语法之后,可以大致绘制出 JSON 部分的状态机,但是由于我们无法确定其他部分是不是符合 JSON 语法(或者说是不是符合完整的 JSON 语法);所以我们整体状态应该分为非 JSON 和 JSON 两种状态。在完整的 JSON 解析中,我们又分为字符串状态和非字符串状态。

注意,上述状态描述并不是正则文法能解析的工作

同时我们发现,字符串状态和 JSON 状态之前的变迁并不是有限状态变迁,需要有上下文参照,因此使用状态栈的入栈出栈实现上下文切换是最合理的实现。

从任意文本提取域名(Domains)#

除此之外,我们也经常需要提取文本中的域名来快速收集一些信息帮助我们做决策。因此我们在 str.ExtractDomain 中也提供了对域名的快速提取功能实现。

我们观察如下案例即可快速了解

input = `HTTP/1.1 302 FoundConnection: closeChild: 1Content-Type: text/html; charset=UTF-8Date: Wed, 15 Feb 2023 07:39:26 GMTLocation: https://1052A00002-0.m.ctrmi.cn/t/ad2?eid=1052A00002&sdr=clt&ac=0&ua=__UA__&os=__OS__&udid=__OPENUDID__&oaid=__OAID__&iesid=__IESID__&ts=__TS__&mac=__MAC__&mac1=__MAC1__&imei=__IMEI__&osv=__OSVS__&adid=__ANDROIDID__&idfa=__IDFA__&ip=__IP__Set-Cookie: _Xdwuv=63ec8c2e208c3; expires=Mon, 14-Aug-2023 16:00:00 GMT; Max-Age=15582034; path=/; domain=.xcar.com.cnSpanid: 1Traceid: 1676********33858396X-Cache: bypassX-Via-Jsl: 76297d3,-X-Via-Svr: tx-click-web-3385X-Via-Svr: tx-public-webproxy-34111Content-Length: 0`domains = str.ExtractDomain(input)
/*OUTPUT:
([]string) (len=3 cap=4) { (string) (len=8) "ctrmi.cn", (string) (len=23) "1052A00002-0.m.ctrmi.cn", (string) (len=11) "xcar.com.cn"}*/

在上述数据包中,观察发现,在 LocationSet-Cookie 中有一些域名信息,这些域名信息其实比较难以提取,正常来说我们需要通过解析数据包的内容取出 Header 信息来操作。

但是实际上,我们如果使用 str.ExtractDomain 就可以直接提取出域名列表。这具有很强的实战价值,能方便我们在打点和做漏洞赏金时更快整理信息和范围。

提取域名数据的技术实现

这个功能不是简单的通过一个或若干正则来实现的,我们使用了一种个状态机来对 "任意" 文本进行词法解析。

在解析到可能符合 "常见域名" 定义的词的时候,进行记录和去重。

同时在这个函数中也会自动处理 "根域名" 相关问题。

所有可用函数列表#

str - 函数集总览