Skip to content

Commit 82de3fc

Browse files
committed
update 2.0.9
1 parent f10ce0c commit 82de3fc

File tree

15 files changed

+229
-81
lines changed

15 files changed

+229
-81
lines changed

core/jsfind/jsfind.go

Lines changed: 86 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import (
1313
"strings"
1414
"sync"
1515

16+
"maps"
17+
1618
"github.com/panjf2000/ants/v2"
1719
"github.com/wailsapp/wails/v2/pkg/runtime"
1820
)
@@ -84,6 +86,7 @@ func ExtractJS(ctx context.Context, url string) (allJS []string) {
8486
content := string(body)
8587
for _, reg := range regJS {
8688
for _, item := range reg.FindAllString(content, -1) {
89+
item = strings.TrimSpace(item)
8790
item = strings.TrimLeft(item, "=")
8891
item = strings.Trim(item, "\"")
8992
item = strings.Trim(item, "'")
@@ -261,8 +264,8 @@ func FilterExt(iss []structs.InfoSource) (news []structs.InfoSource) {
261264
// 处理 API 逻辑
262265
func AnalyzeAPI(ctx context.Context, o structs.JSFindOptions) {
263266
resp, err := clients.SimpleGet(o.HomeURL, clients.NewRestyClient(nil, true))
264-
if err != nil || resp == nil {
265-
gologger.Error(ctx, fmt.Sprintf("[AnalyzeAPI] %s, error: %v", o.HomeURL, err))
267+
if err != nil {
268+
gologger.Error(ctx, fmt.Sprintf("[AnalyzeAPI] 请求首页失败 %s, 错误: %v", o.HomeURL, err))
266269
return
267270
}
268271
homeBody := string(resp.Body())
@@ -272,59 +275,127 @@ func AnalyzeAPI(ctx context.Context, o structs.JSFindOptions) {
272275
defer wg.Done()
273276
api := data.(string)
274277

278+
// 为每个 API 独立生成 fullURL 和 headers 副本
275279
fullURL := strings.TrimRight(o.BaseURL, "/") + "/" + strings.TrimLeft(api, "/")
276-
method, err := detectMethod(fullURL, o.Headers)
280+
281+
// 拷贝 headers,避免竞争
282+
apiHeaders := make(map[string]string)
283+
maps.Copy(apiHeaders, o.Headers)
284+
285+
// 检测请求方法
286+
method, err := detectMethod(fullURL, apiHeaders)
277287
if err != nil {
278-
runtime.EventsEmit(ctx, "jsfindlog", fmt.Sprintf("[!] %s : %v", fullURL, err))
288+
runtime.EventsEmit(ctx, "jsfindlog", fmt.Sprintf("[!] %s 检测请求方法失败: %v", fullURL, err))
279289
return
280290
}
281291

292+
// 如果是 POST 方法,动态探测 Content-Type
282293
if method == http.MethodPost {
283-
contentType := detectContentType(fullURL, o.Headers)
284-
if contentType != "" {
285-
o.Headers["Content-Type"] = contentType
294+
if contentType := detectContentType(fullURL, apiHeaders); contentType != "" {
295+
apiHeaders["Content-Type"] = contentType
286296
}
287297
}
288298

299+
// 补全参数
289300
param := completeParameters(method, fullURL, url.Values{})
290301
if param != nil {
291-
runtime.EventsEmit(ctx, "jsfindlog", fmt.Sprintf("[+] %s 已补全参数 %s", fullURL, param.Encode()))
302+
runtime.EventsEmit(ctx, "jsfindlog", fmt.Sprintf("[+] %s 已补全参数: %s", fullURL, param.Encode()))
292303
}
293304

305+
// 构建请求对象
294306
apiReq := APIRequest{
295307
URL: fullURL,
296308
Method: method,
297-
Headers: o.Headers,
309+
Headers: apiHeaders,
298310
Params: param,
299311
}
312+
313+
// 检查高风险路由,直接跳过测试
300314
for _, router := range o.HighRiskRouter {
301315
if strings.Contains(apiReq.URL, router) {
302-
runtime.EventsEmit(ctx, "jsfindlog", "[!!] "+fullURL+" 高风险API已跳过测试, 相关敏感词: "+router)
316+
runtime.EventsEmit(ctx, "jsfindlog", "[!!] "+fullURL+" 高风险API跳过测试, 触发敏感词: "+router)
303317
return
304318
}
305319
}
320+
306321
// 测试未授权访问
307322
vulnerable, body, err := testUnauthorizedAccess(homeBody, apiReq, o.Authentication)
308-
if err != nil || !vulnerable {
309-
runtime.EventsEmit(ctx, "jsfindlog", "[-] "+fullURL+" 不存在未授权")
323+
if err != nil {
324+
runtime.EventsEmit(ctx, "jsfindlog", fmt.Sprintf("[-] %s 测试未授权错误: %v", fullURL, err))
325+
return
326+
}
327+
328+
if !vulnerable {
329+
runtime.EventsEmit(ctx, "jsfindlog", "[-] "+fullURL+" 不存在未授权访问")
310330
return
311331
}
312-
runtime.EventsEmit(ctx, "jsfindlog", "[+] "+fullURL+" 存在未授权")
332+
333+
// 存在未授权,记录漏洞信息
334+
runtime.EventsEmit(ctx, "jsfindlog", "[+] "+fullURL+" 存在未授权访问!")
313335
runtime.EventsEmit(ctx, "jsfindvulcheck", structs.JSFindResult{
314336
Method: method,
315-
Param: param.Encode(),
337+
Request: buildRawRequest(apiReq),
316338
Source: fullURL,
317339
Response: string(body),
318340
Length: len(body),
319341
})
320342
})
321343
defer pool.Release()
322344

323-
// 提交 API 任务到线程池
345+
// 提交任务
324346
for _, api := range o.ApiList {
325347
wg.Add(1)
326348
pool.Invoke(api)
327349
}
328350

329351
wg.Wait()
330352
}
353+
354+
func buildRawRequest(req APIRequest) string {
355+
var sb strings.Builder
356+
357+
parsedURL, err := url.Parse(req.URL)
358+
if err != nil {
359+
return "" // URL解析失败直接返回空
360+
}
361+
362+
// 如果是 GET 请求,需要将参数附加到 URL 后面
363+
if req.Method == http.MethodGet && req.Params != nil {
364+
// 编码查询参数并将其附加到 URL
365+
queryString := req.Params.Encode()
366+
if parsedURL.RawQuery == "" {
367+
parsedURL.RawQuery = queryString
368+
} else {
369+
parsedURL.RawQuery += "&" + queryString
370+
}
371+
}
372+
373+
// 写入请求行,只保留Path+Query,不要域名
374+
pathWithQuery := parsedURL.Path
375+
if parsedURL.RawQuery != "" {
376+
pathWithQuery += "?" + parsedURL.RawQuery
377+
}
378+
sb.WriteString(fmt.Sprintf("%s %s HTTP/1.1\n", req.Method, pathWithQuery))
379+
380+
// Host 头
381+
sb.WriteString(fmt.Sprintf("Host: %s\n", parsedURL.Host))
382+
383+
// 其他 Headers
384+
for k, v := range req.Headers {
385+
// 避免重复写 Host
386+
if strings.ToLower(k) == "host" {
387+
continue
388+
}
389+
sb.WriteString(fmt.Sprintf("%s: %s\n", k, v))
390+
}
391+
392+
sb.WriteString("\n") // 头结束空一行
393+
394+
// 写入请求体
395+
if req.Method != http.MethodGet && req.Params != nil {
396+
// 如果是非 GET 请求,参数放到请求体中
397+
sb.WriteString(req.Params.Encode())
398+
}
399+
400+
return sb.String()
401+
}

core/jsfind/jsfind_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
package jsfind
22

33
import (
4+
"fmt"
45
"testing"
56
)
67

78
func TestJSFInd(t *testing.T) {
8-
9+
result := detectContentType("http://api", nil)
10+
fmt.Println(result)
911
}

core/jsfind/method.go

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,20 @@ package jsfind
22

33
import (
44
"errors"
5+
"maps"
56
"slack-wails/lib/clients"
67
"strings"
78
)
89

910
func detectMethod(fullURL string, headers map[string]string) (string, error) {
1011
resp, err := clients.DoRequest("GET", fullURL, headers, nil, 5, clients.NewRestyClient(nil, true))
11-
body := string(resp.Body())
1212
if err != nil {
1313
if strings.Contains(err.Error(), "doesn't contain any IP SANs") {
1414
return "", errors.New("证书中不包含使用的域名/IP, 请求失败")
1515
}
1616
return "", err
1717
}
18-
if resp == nil {
19-
return "", errors.New("响应内容为空")
20-
}
18+
body := string(resp.Body())
2119
// 模式错误情况 1
2220
if (strings.Contains(body, "not supported") && strings.Contains(body, "Request method")) || resp.StatusCode() == 405 {
2321
return "POST", nil
@@ -35,40 +33,45 @@ func detectMethod(fullURL string, headers map[string]string) (string, error) {
3533
}
3634

3735
func detectContentType(url string, headers map[string]string) string {
38-
// 先浅拷贝一下headers,避免污染原map
36+
// 先浅拷贝一下 headers,避免污染原 headers
3937
hdr := make(map[string]string)
40-
for k, v := range headers {
41-
hdr[k] = v
42-
}
38+
maps.Copy(hdr, headers)
4339

44-
// 第一次,不带 Content-Type 测试
40+
// 第一次,不带 Content-Type 直接测试
4541
resp, err := clients.DoRequest("POST", url, hdr, nil, 10, clients.NewRestyClient(nil, true))
46-
if err != nil || resp == nil {
42+
if err != nil {
4743
return ""
4844
}
4945

50-
// 如果第一次就能正常POST,不需要加Content-Type,默认返回 application/x-www-form-urlencoded
51-
if resp.StatusCode() >= 200 && resp.StatusCode() < 300 {
52-
return "application/x-www-form-urlencoded"
46+
body := string(resp.Body())
47+
48+
if strings.Contains(body, "not a multipart request") {
49+
return "multipart/form-data;boundary=8ce4b16b22b58894aa86c421e8759df3"
5350
}
5451

55-
// 如果出错,或者响应提示Content-Type不对,再带上 application/x-www-form-urlencoded 重试一次
52+
// 参数体缺失,一般都为json请求
53+
if strings.Contains(body, "Required request body is missing") {
54+
return "application/json"
55+
}
56+
57+
// 第一次响应:如果返回 text/plain不支持则需要 content-type 字段
58+
if !strings.Contains(body, "not supported") && !strings.Contains(body, "text/plain") {
59+
return "text/plain"
60+
}
61+
62+
// 需要重试,带上 application/x-www-form-urlencoded 重新请求
5663
hdr["Content-Type"] = "application/x-www-form-urlencoded"
5764
resp, err = clients.DoRequest("POST", url, hdr, nil, 10, clients.NewRestyClient(nil, true))
58-
if err != nil || resp == nil {
65+
if err != nil {
5966
return ""
6067
}
61-
body := string(resp.Body())
6268

63-
// 判断返回内容
69+
body = string(resp.Body())
70+
6471
if strings.Contains(body, "application/x-www-form-urlencoded") && strings.Contains(body, "not supported") {
6572
return "application/json"
6673
}
6774

68-
if strings.Contains(body, "not a multipart request") {
69-
return "multipart/form-data;boundary=8ce4b16b22b58894aa86c421e8759df3"
70-
}
71-
72-
// 默认
75+
// 默认返回 application/x-www-form-urlencoded
7376
return "application/x-www-form-urlencoded"
7477
}

core/jsfind/openai.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package jsfind
2+
3+
// type UnauthorizedChecker struct {
4+
// client *openai.Client
5+
// messages []openai.ChatCompletionMessageParamUnion
6+
// model string
7+
// ctx context.Context
8+
// }
9+
10+
// // 定义最大并发数
11+
// const maxConcurrentRequests = 2
12+
13+
// // 用于控制并发的信号通道
14+
// var sem = make(chan struct{}, maxConcurrentRequests)
15+
16+
// // NewChecker 初始化
17+
// func NewChecker(apiKey, baseURL, model string) *UnauthorizedChecker {
18+
// client := openai.NewClient(
19+
// option.WithAPIKey(apiKey),
20+
// option.WithBaseURL(baseURL),
21+
// option.WithHTTPClient(&http.Client{
22+
// Timeout: 5 * time.Second,
23+
// }),
24+
// )
25+
// return &UnauthorizedChecker{
26+
// client: &client,
27+
// model: model,
28+
// ctx: context.Background(),
29+
// messages: []openai.ChatCompletionMessageParamUnion{
30+
// openai.UserMessage("你是一个未授权访问检测助手,用户会提供一些接口的返回数据,你需要判断接口的返回数据来判断接口是否需要用户登录。回答用true/false,除了true或false不要输出任何其他内容。"),
31+
// },
32+
// }
33+
// }
34+
35+
// // Check 输入响应文本,返回是否未授权访问
36+
// func (c *UnauthorizedChecker) Check(responseBody string) (bool, error) {
37+
// // 获取并发许可
38+
// sem <- struct{}{}
39+
// defer func() {
40+
// // 释放并发许可
41+
// <-sem
42+
// }()
43+
// // 加入新的用户输入
44+
// c.messages = append(c.messages, openai.UserMessage(responseBody))
45+
// chatCompletion, err := c.client.Chat.Completions.New(c.ctx, openai.ChatCompletionNewParams{
46+
// Messages: c.messages,
47+
// Model: c.model,
48+
// })
49+
// if err != nil {
50+
// return false, err
51+
// }
52+
53+
// // 安全处理,防止下标越界
54+
// if len(chatCompletion.Choices) == 0 {
55+
// return false, fmt.Errorf("no choices returned from model")
56+
// }
57+
58+
// reply := strings.TrimSpace(chatCompletion.Choices[0].Message.Content)
59+
60+
// // 只判断是否返回 true
61+
// return strings.ToLower(reply) == "true", nil
62+
// }

core/jsfind/param.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ import (
55
"net/url"
66
"regexp"
77
"slack-wails/lib/clients"
8+
"strings"
89
)
910

1011
type Parameter struct {
1112
Name string `json:"name"`
1213
Type string `json:"type"`
1314
}
1415

15-
var extractMissingRegex = regexp.MustCompile(`Required (String|Int|Long|Double|Boolean|Date|ArrayList).*?'([^']+)'`)
16+
var extractMissingRegex = regexp.MustCompile(`(?i)required (string|int|long|double|boolean|date|arraylist).*?'([^']+)'`)
1617

1718
// 从错误信息中提取缺失参数的名称
1819
func extractMissingParams(message string) *Parameter {
@@ -30,20 +31,20 @@ func extractMissingParams(message string) *Parameter {
3031

3132
// 根据参数类型生成默认值
3233
func generateDefaultValue(paramType string) interface{} {
33-
switch paramType {
34-
case "String":
34+
switch strings.ToLower(paramType) {
35+
case "string":
3536
return "test"
36-
case "Int":
37+
case "int":
3738
return 0
38-
case "Long":
39+
case "long":
3940
return int64(0)
40-
case "Double":
41+
case "double":
4142
return 0.0
42-
case "Boolean":
43+
case "boolean":
4344
return false
44-
case "Date":
45+
case "date":
4546
return "1970-01-01"
46-
case "ArrayList":
47+
case "arraylist":
4748
return []string{"1"}
4849
default:
4950
return "defaultValue"

0 commit comments

Comments
 (0)