1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
|
---
-- The brute library is an attempt to create a common framework for performing
-- password guessing against remote services.
--
-- The library currently attempts to parallelize the guessing by starting
-- a number of working threads and increasing that number gradually until
-- brute.threads limit is reached. The starting number of threads can be set
-- with brute.start argument, it defaults to 5. The brute.threads argument
-- defaults to 20. It is worth noticing that the number of working threads
-- will grow exponentially until any error occurs, after that the engine
-- will switch to linear growth.
--
-- The library contains the following classes:
-- * <code>Engine</code>
-- ** The actual engine doing the brute-forcing .
-- * <code>Error</code>
-- ** Class used to return errors back to the engine.
-- * <code>Options</code>
-- ** Stores any options that should be used during brute-forcing.
--
-- In order to make use of the framework a script needs to implement a Driver
-- class. The Driver class is then to be passed as a parameter to the Engine
-- constructor, which creates a new instance for each guess. The Driver class
-- SHOULD implement the following four methods:
--
-- <code>
-- Driver:login = function( self, username, password )
-- Driver:check = function( self )
-- Driver:connect = function( self )
-- Driver:disconnect = function( self )
-- </code>
--
-- The <code>login</code> method does not need a lot of explanation. The login
-- function should return two parameters. If the login was successful it should
-- return true and a <code>creds.Account</code>. If the login was a failure it
-- should return false and a <code>brute.Error</code>. The driver can signal
-- the Engine to retry a set of credentials by calling the Error objects
-- <code>setRetry</code> method. It may also signal the Engine to abort all
-- password guessing by calling the Error objects <code>setAbort</code> method.
-- Finally, the driver can notify the Engine about protocol related exception
-- (like the ftp code 421 "Too many connections") by calling
-- <code>setReduce</code> method. The latter will signal the Engine to reduce
-- the number of running worker threads.
--
-- The following example code demonstrates how the Error object can be used.
--
-- <code>
-- -- After a number of incorrect attempts VNC blocks us, so we abort
-- if ( not(status) and x:match("Too many authentication failures") ) then
-- local err = brute.Error:new( data )
-- -- signal the engine to abort
-- err:setAbort( true )
-- return false, err
-- elseif ( not(status) ) then
-- local err = brute.Error:new( "VNC handshake failed" )
-- -- This might be temporary, signal the engine to retry
-- err:setRetry( true )
-- return false, err
-- end
-- .
-- .
-- .
-- -- Return a simple error, no retry needed
-- return false, brute.Error:new( "Incorrect password" )
-- </code>
--
-- The purpose of the <code>check</code> method is to be able to determine
-- whether the script has all the information it needs, before starting the
-- brute force. It's the method where you should check, e.g., if the correct
-- database or repository URL was specified or not. On success, the
-- <code>check</code> method returns true, on failure it returns false and the
-- brute force engine aborts.
--
-- NOTE: The <code>check</code> method is deprecated and will be removed from
-- all scripts in the future. Scripts should do this check in the action
-- function instead.
--
-- The <code>connect</code> method provides the framework with the ability to
-- ensure that the thread can run once it has been dispatched a set of
-- credentials. As the sockets in NSE are limited we want to limit the risk of
-- a thread blocking, due to insufficient free sockets, after it has acquired a
-- username and password pair.
--
-- The following sample code illustrates how to implement a sample
-- <code>Driver</code> that sends each username and password over a socket.
--
-- <code>
-- Driver = {
-- new = function(self, host, port, options)
-- local o = {}
-- setmetatable(o, self)
-- self.__index = self
-- o.host = host
-- o.port = port
-- o.options = options
-- return o
-- end,
-- connect = function( self )
-- self.socket = nmap.new_socket()
-- return self.socket:connect( self.host, self.port )
-- end,
-- disconnect = function( self )
-- return self.socket:close()
-- end,
-- check = function( self )
-- return true
-- end,
-- login = function( self, username, password )
-- local status, err, data
-- status, err = self.socket:send( username .. ":" .. password)
-- status, data = self.socket:receive_bytes(1)
--
-- if ( data:match("SUCCESS") ) then
-- return true, creds.Account:new(username, password, creds.State.VALID)
-- end
-- return false, brute.Error:new( "login failed" )
-- end,
-- }
-- </code>
--
-- The following sample code illustrates how to pass the <code>Driver</code>
-- off to the brute engine.
--
-- <code>
-- action = function(host, port)
-- local options = { key1 = val1, key2 = val2 }
-- local status, accounts = brute.Engine:new(Driver, host, port, options):start()
-- if( not(status) ) then
-- return accounts
-- end
-- return stdnse.format_output( true, accounts )
-- end
-- </code>
--
-- The Engine is written with performance and reasonable resource usage in mind
-- and requires minimum extra work from a script developer. A trivial approach
-- is to spawn as many working threads as possible regardless of network
-- conditions, other scripts' needs, and protocol response. This indeed works
-- well, but only in ideal conditions. In reality there might be several
-- scripts running or only limited number of threads are allowed to use sockets
-- at any given moment (as it is in Nmap). A more intelligent approach is to
-- automate the management of Engine's running threads, so that performance
-- of other scripts does not suffer because of exhaustive brute force work.
-- This can be done on three levels: protocol, network, and resource level.
--
-- On the protocol level the developer should notify the Engine about connection
-- restrictions imposed by a server that can be learned during a protocol
-- communication. Like code 421 "To many connections" is used in FTP. Reasonably
-- in such cases we would like to reduce the number of connections to this
-- service, hence saving resources for other work and reducing the load on the
-- target server. This can be done by returning an Error object with called
-- <code>setReduce</code> method on it. The error will make the Engine reduce
-- the number of running threads.
--
-- Following is an example how it can be done for FTP brute.
--
-- <code>
-- local line = <response from the server>
--
-- if(string.match(line, "^230")) then
-- stdnse.debug1("Successful login: %s/%s", user, pass)
-- return true, creds.Account:new( user, pass, creds.State.VALID)
-- elseif(string.match(line, "^530")) then
-- return false, brute.Error:new( "Incorrect password" )
-- elseif(string.match(line, "^421")) then
-- local err = brute.Error:new("Too many connections")
-- err:setReduce(true)
-- return false, err
-- elseif(string.match(line, "^220")) then
-- elseif(string.match(line, "^331")) then
-- else
-- stdnse.debug1("WARNING: Unhandled response: %s", line)
-- local err = brute.Error:new("Unhandled response")
-- err:setRetry(true)
-- return false, err
-- end
-- </code>
--
-- On the network level we want to catch errors that can occur because of
-- network congestion or target machine specifics, say firewalled. These
-- errors can be caught as return results of operations on sockets, like
-- <code>local status, err = socket.receive()</code>. Asking a developer to
-- relay such errors to the Engine is counterproductive, and it would lead to
-- bloated scripts with lots of repetitive code. The Engine takes care of that
-- with a little help from the developer. The only thing that needs to be
-- done is to use <code>brute.new_socket()</code> instead of
-- <code>nmap.new_socket()</code> when creating a socket in a script.
--
-- NOTE: A socket created with <code>brute.new_socket()</code> will behave as
-- a regular socket when used without the brute library. The returned object
-- is a BruteSocket instance, which can be treated as a regular socket object.
--
-- Example on creating "brute" socket.
--
-- <code>
-- connect = function( self )
-- self.socket = brute.new_socket()
-- local status, err = self.socket:connect(self.host, self.port)
-- self.socket:set_timeout(arg_timeout)
-- if(not(status)) then
-- return false, brute.Error:new( "Couldn't connect to host: " .. err )
-- end
-- return true
-- end
-- </code>
--
-- On the resource level the Engine can query the current status of the NSE.
-- As of the time of writing, the only parameter used is a number of threads
-- waiting for connection (as was said before the NSE has a constraint on the
-- number of concurrent connections due to performance reasons). With a
-- running brute script the limit can be hit pretty fast, which can affect
-- performance of other scripts. To mitigate this situation resource management
-- strategy is used, and the Engine will reduce the number of working threads
-- if there are any threads waiting for connection. As a result the preference
-- for connection will be given to non brute scripts and if there are many
-- brute scripts running simultaneously, then they will not exhaust resources
-- unnecessarily.
-- This feature is enabled by default and does not require any additional work
-- from the developer.
--
-- Stagnation avoidance mechanism is implemented to alert users about services
-- that might have failed during bruteforcing. The Engine will abort if all working
-- threads have been experiencing connection errors during 100 consequentive
-- iterations of the main thread loop. If <code>brute.killstagnated</code>
-- is set to <code>false</code> the Engine will continue after issuing a
-- warning.
--
-- For a complete example of a brute implementation consult the
-- <code>svn-brute.nse</code> or <code>vnc-brute.nse</code> scripts
--
-- @args brute.useraspass guess the username as password for each user
-- (default: true)
-- @args brute.emptypass guess an empty password for each user
-- (default: false)
-- @args brute.unique make sure that each password is only guessed once
-- (default: true)
-- @args brute.firstonly stop guessing after first password is found
-- (default: false)
-- @args brute.passonly iterate over passwords only for services that provide
-- only a password for authentication. (default: false)
-- @args brute.retries the number of times to retry if recoverable failures
-- occur. (default: 2)
-- @args brute.delay the number of seconds to wait between guesses (default: 0)
-- @args brute.threads the number of initial worker threads, the number of
-- active threads will be automatically adjusted.
-- @args brute.mode can be user, pass or creds and determines what mode to run
-- the engine in.
-- * user - the unpwdb library is used to guess passwords, every password
-- password is tried for each user. (The user iterator is in the
-- outer loop)
-- * pass - the unpwdb library is used to guess passwords, each password
-- is tried for every user. (The password iterator is in the
-- outer loop)
-- * creds- a set of credentials (username and password pairs) are
-- guessed against the service. This allows for lists of known
-- or common username and password combinations to be tested.
-- If no mode is specified and the script has not added any custom
-- iterator the pass mode will be enabled.
-- @args brute.credfile a file containing username and password pairs delimited
-- by '/'
-- @args brute.guesses the number of guesses to perform against each account.
-- (default: 0 (unlimited)). The argument can be used to prevent account
-- lockouts.
-- @args brute.start the number of threads the engine will start with.
-- (default: 5).
--
-- @author Patrik Karlsson <patrik@cqure.net>
-- @copyright Same as Nmap--See https://nmap.org/book/man-legal.html
--
-- Version 0.73
-- Created 06/12/2010 - v0.1 - created by Patrik Karlsson <patrik@cqure.net>
-- Revised 07/13/2010 - v0.2 - added connect, disconnect methods to Driver
-- <patrik@cqure.net>
-- Revised 07/21/2010 - v0.3 - documented missing argument brute.mode
-- Revised 07/23/2010 - v0.4 - fixed incorrect statistics and changed output to
-- include statistics, and to display "no accounts
-- found" message.
-- Revised 08/14/2010 - v0.5 - added some documentation and smaller changes per
-- David's request.
-- Revised 08/30/2010 - v0.6 - added support for custom iterators and did some
-- needed cleanup.
-- Revised 06/19/2011 - v0.7 - added support for creds library [Patrik]
-- Revised 07/07/2011 - v0.71- fixed some minor bugs, and changed credential
-- iterator to use a file handle instead of table
-- Revised 07/21/2011 - v0.72- added code to allow script reporting invalid
-- (non existing) accounts using setInvalidAccount
-- Revised 11/12/2011 - v0.73- added support for max guesses per account to
-- prevent account lockouts.
-- bugfix: added support for guessing the username
-- as password per default, as suggested by the
-- documentation.
-- Revised 07/11/2016 - v.8 - added smart resource management and error handling
-- mechanisms. Sergey Khegay <g.sergeykhegay@gmail.com>
local coroutine = require "coroutine"
local creds = require "creds"
local io = require "io"
local nmap = require "nmap"
local os = require "os"
local stdnse = require "stdnse"
local table = require "table"
local unpwdb = require "unpwdb"
local math = require "math"
_ENV = stdnse.module("brute", stdnse.seeall)
-- Engine options that can be set by scripts
-- Supported options are:
-- * firstonly - stop after finding the first correct password
-- (can be set using script-arg brute.firstonly)
-- * passonly - guess passwords only, don't supply a username
-- (can be set using script-arg brute.passonly)
-- * max_retries - the amount of retries to do before aborting
-- (can be set using script-arg brute.retries)
-- * delay - sets the delay between attempts
-- (can be set using script-arg brute.delay)
-- * mode - can be set to either creds, user or pass and controls
-- whether the engine should iterate over users, passwords
-- or fetch a list of credentials from a single file.
-- (can be set using script-arg brute.mode)
-- * title - changes the title of the result table where the
-- passwords are returned.
-- * nostore - don't store the results in the credential library
-- * max_guesses - the maximum amount of guesses to perform for each
-- account.
-- * useraspass - guesses the username as password (default: true)
-- * emptypass - guesses an empty string as password (default: false)
-- * killstagnated - abort the Engine if bruteforcing has stagnated
-- getting too many connections errors. (default: true)
--
Options = {
new = function (self)
local o = {}
setmetatable(o, self)
self.__index = self
o.emptypass = self.checkBoolArg("brute.emptypass", false)
o.useraspass = self.checkBoolArg("brute.useraspass", true)
o.firstonly = self.checkBoolArg("brute.firstonly", false)
o.passonly = self.checkBoolArg("brute.passonly", false)
o.killstagnated = self.checkBoolArg("brute.killstagnated", true)
o.max_retries = tonumber(stdnse.get_script_args("brute.retries")) or 2
o.delay = tonumber(stdnse.get_script_args("brute.delay")) or 0
o.max_guesses = tonumber(stdnse.get_script_args("brute.guesses")) or 0
return o
end,
--- Checks if a script argument is boolean true or false
--
-- @param arg string containing the name of the argument to check
-- @param default boolean containing the default value
-- @return boolean, true if argument evaluates to 1 or true, else false
checkBoolArg = function (arg, default)
local val = stdnse.get_script_args(arg) or default
return (val == "true" or val == true or tonumber(val) == 1)
end,
--- Sets the brute mode to either iterate over users or passwords
-- @see description for more information.
--
-- @param mode string containing either "user" or "password"
-- @return status true on success else false
-- @return err string containing the error message on failure
setMode = function (self, mode)
local modes = {
"password",
"user",
"creds",
}
local supported = false
for _, m in ipairs(modes) do
if mode == m then
supported = true
end
end
if not supported then
stdnse.debug1("ERROR: brute.options.setMode: mode %s not supported", mode)
return false, "Unsupported mode"
else
self.mode = mode
end
return true
end,
--- Sets an option parameter
--
-- @param param string containing the parameter name
-- @param value string containing the parameter value
setOption = function (self, param, value)
self[param] = value
end,
--- Set an alternate title for the result output (default: Accounts)
--
-- @param title string containing the title value
setTitle = function (self, title)
self.title = title
end,
}
-- The account object which is to be reported back from each driver
-- The Error class, is currently only used to flag for retries
-- It also contains the error message, if one was returned from the driver.
Error = {
retry = false,
new = function (self, msg)
local o = {
msg = msg,
done = false,
reduce = nil,
}
setmetatable(o, self)
self.__index = self
return o
end,
--- Is the error recoverable?
--
-- @return status true if the error is recoverable, false if not
isRetry = function (self)
return self.retry
end,
--- Set the error as recoverable
--
-- @param r boolean true if the engine should attempt to retry the
-- credentials, unset or false if not
setRetry = function (self, r)
self.retry = r
end,
--- Set the error as abort all threads
--
-- @param b boolean true if the engine should abort guessing on all threads
setAbort = function (self, b)
self.abort = b
end,
--- Was the error abortable
--
-- @return status true if the driver flagged the engine to abort
isAbort = function (self)
return self.abort
end,
--- Get the error message reported
--
-- @return msg string containing the error message
getMessage = function (self)
return self.msg
end,
--- Is the thread done?
--
-- @return status true if done, false if not
isDone = function (self)
return self.done
end,
--- Signals the engine that the thread is done and should be terminated
--
-- @param b boolean true if done, unset or false if not
setDone = function (self, b)
self.done = b
end,
-- Marks the username as invalid, aborting further guessing.
-- @param username
setInvalidAccount = function (self, username)
self.invalid_account = username
end,
-- Checks if the error reported the account as invalid.
-- @return username string containing the invalid account
isInvalidAccount = function (self)
return self.invalid_account
end,
--- Set the error as reduce the number of running threads
--
-- @param r boolean true if should reduce, unset or false if not
setReduce = function (self, r)
self.reduce = r
end,
--- Checks if the error signals to reduce the number of running threads
--
-- @return status true if reduce, false otherwise
isReduce = function (self)
if self.reduce then
return true
end
return false
end,
}
-- Auxillary data structure
Batch = {
new = function (self, lim, stime)
local o = {
limit = lim or 3, -- maximum number of items
full = false,
data = {}, -- storage
size = 0, -- current number of items
start_time = stime or 0,
}
setmetatable(o, self)
self.__index = self
return o
end,
--- Adds new item to the vault (if possible)
--
-- @param obj, new object
-- @return true if insert is successful, false if the vault is full
add = function (self, obj)
if self.size < self.limit then
self.data[self.size + 1] = obj
self.size = self.size + 1
return true
end
return false
end,
isFull = function (self)
if self.size >= self.limit then
return true
end
return false
end,
getData = function (self)
return self.data
end,
getSize = function (self)
return self.size
end,
getStartTime = function (self)
return self.start_time
end,
getLimit = function (self)
return self.limit
end,
setLimit = function (self, lim)
self.limit = lim
end,
}
-- The brute engine, doing all the nasty work
Engine = {
STAT_INTERVAL = 20,
THREAD_TO_ENGINE = {},
--- Creates a new Engine instance
--
-- @param driver, the driver class that should be instantiated
-- @param host table as passed to the action method of the script
-- @param port table as passed to the action method of the script
-- @param options table containing any script specific options
-- @return o new Engine instance
new = function (self, driver, host, port, options)
-- we want Engine.THREAD_TO_ENGINE to contain weak keys
-- for effective garbage collection
if getmetatable(Engine.THREAD_TO_ENGINE) == nil then
setmetatable(Engine.THREAD_TO_ENGINE, {
__mode = "k",
})
end
local o = {
driver = driver,
host = host,
port = port,
driver_options = options,
terminate_all = false,
error = nil,
counter = 0,
threads = {},
tps = {},
iterator = nil,
usernames = usernames_iterator(),
passwords = passwords_iterator(),
found_accounts = {},
account_guesses = {},
options = Options:new(),
retry_accounts = {},
initial_accounts_exhausted = false,
batch = nil,
tick = 0,
}
setmetatable(o, self)
self.__index = self
o.max_threads = tonumber(stdnse.get_script_args "brute.threads") or 20
o.start_threads = tonumber(stdnse.get_script_args "brute.start") or 5
return o
end,
--- Sets the username iterator
--
-- @param usernameIterator function to set as a username iterator
setUsernameIterator = function (self, usernameIterator)
self.usernames = usernameIterator
end,
--- Sets the password iterator
--
-- @param passwordIterator function to set as a password iterator
setPasswordIterator = function (self, passwordIterator)
self.passwords = passwordIterator
end,
--- Limit the number of worker threads
--
-- @param max number containing the maximum number of allowed threads
setMaxThreads = function (self, max)
self.max_threads = max
end,
--- Returns the number of non-dead threads
--
-- @return count number of non-dead threads
threadCount = function (self)
local count = 0
for thread in pairs(self.threads) do
if coroutine.status(thread) == "dead" then
self.threads[thread] = nil
else
count = count + 1
end
end
return count
end,
--- Calculates the number of threads that are actually doing any work
--
-- @return count number of threads performing activity
activeThreads = function (self)
local count = 0
for thread, v in pairs(self.threads) do
if v.guesses ~= nil then
count = count + 1
end
end
return count
end,
--- Iterator wrapper used to iterate over all registered iterators
--
-- @return iterator function
get_next_credential = function (self)
local function next_credential ()
for user, pass in self.iterator do
-- makes sure the credentials have not been tested before
self.used_creds = self.used_creds or {}
pass = pass or "nil"
if not self.used_creds[user .. pass] then
self.used_creds[user .. pass] = true
coroutine.yield(user, pass)
end
end
while true do
coroutine.yield(nil, nil)
end
end
return coroutine.wrap(next_credential)
end,
--- Does the actual authentication request
--
-- @return true on success, false on failure
-- @return response Account on success, Error on failure
doAuthenticate = function (self)
local status, response
local next_credential = self:get_next_credential()
local tries = self.options.max_retries + 1
local username, password
local thread_data = Engine.getThreadData(coroutine.running())
assert(thread_data, "Unknown coroutine is running")
repeat
local driver = self.driver:new(self.host, self.port, self.driver_options)
status, response = driver:connect()
-- Temporary workaround. Did not connect successfully
-- due to stressed server
if not status then
-- We have to first check whether the response is a brute.Error
-- since many times the connect method returns a string error instead,
-- which could crash this thread in several places
if response and not response.isReduce then
-- Create a new Error
response = Error:new("Connect error: " .. response)
response:setRetry(true)
end
if response and response:isReduce() then
local ret_creds = {}
ret_creds.connect_phase = true
return false, response, ret_creds
end
else
-- Did we successfully connect?
if not username and not password then
repeat
if #self.retry_accounts > 0 then
-- stdnse.debug1("Using retry credentials")
username = self.retry_accounts[#self.retry_accounts].username
password = self.retry_accounts[#self.retry_accounts].password
table.remove(self.retry_accounts, #self.retry_accounts)
else
username, password = next_credential()
end
thread_data.username = username
thread_data.password = password
if not username and not password then
driver:disconnect()
self.initial_accounts_exhausted = true
return false
end
until (not self.found_accounts or not self.found_accounts[username])
and (self.options.max_guesses == 0 or not self.account_guesses[username]
or self.options.max_guesses > self.account_guesses[username])
-- increases the number of guesses for an account
self.account_guesses[username] = self.account_guesses[username]
and self.account_guesses[username] + 1 or 1
end
-- make sure that all threads locked in connect stat terminate quickly
if Engine.terminate_all then
driver:disconnect()
driver = nil
return false
end
local c
-- Do we have a username or not?
if username and #username > 0 then
c = ("%s/%s"):format(username, #password > 0 and password or "<empty>")
else
c = ("%s"):format(#password > 0 and password or "<empty>")
end
local msg = (tries <= self.options.max_retries) and "Re-trying" or "Trying"
stdnse.debug2("%s %s against %s:%d", msg, c, self.host.ip, self.port.number)
status, response = driver:login(username, password)
driver:disconnect()
driver = nil
if not status and response and response:isReduce() then
local ret_creds = {}
ret_creds.username = username
ret_creds.password = password
return false, response, ret_creds
end
end
tries = tries - 1
-- End if:
-- * The guess was successful
-- * The response was not set to retry
-- * We've reached the maximum retry attempts
until status or (response and not (response:isRetry())) or tries <= 0
-- Increase the amount of total guesses
self.counter = self.counter + 1
return status, response
end,
login = function (self, cvar)
local condvar = nmap.condvar(cvar)
local thread_data = self.threads[coroutine.running()]
local interval_start = os.time()
while true do
-- Should we terminate all threads or this particular thread?
if (self.terminate_all or thread_data.terminate)
or (self.initial_accounts_exhausted and #self.retry_accounts == 0) then
break
end
-- Update tick and add this thread to the batch
self.tick = self.tick + 1
if not (self.batch:isFull()) and not thread_data.in_batch then
self.batch:add(coroutine.running())
thread_data.in_batch = true
thread_data.ready = false
end
-- We expect doAuthenticate to pass the report variable received from the script
local status, response, ret_creds = self:doAuthenticate()
if thread_data.in_batch then
thread_data.ready = true
end
if status then
-- Prevent locked accounts from appearing several times
if not self.found_accounts or self.found_accounts[response.username] == nil then
if not self.options.nostore then
local c = creds.Credentials:new(self.options.script_name, self.host, self.port)
c:add(response.username, response.password, response.state)
else
self.credstore = self.credstore or {}
table.insert(self.credstore, tostring(response))
end
stdnse.debug1("Discovered account: %s", tostring(response))
-- if we're running in passonly mode, and want to continue guessing
-- we will have a problem as the username is always the same.
-- in this case we don't log the account as found.
if not self.options.passonly then
self.found_accounts[response.username] = true
end
-- Check if firstonly option was set, if so abort all threads
if self.options.firstonly then
self.terminate_all = true
end
end
elseif ret_creds then
if not ret_creds.connect_phase then
-- add credentials to a vault
self.retry_accounts[#self.retry_accounts + 1] = {
username = ret_creds.username,
password = ret_creds.password,
}
end
-- notify the main thread that there were an error on this coroutine
thread_data.protocol_error = true
condvar "signal"
condvar "wait"
else
if response and response:isAbort() then
self.terminate_all = true
self.error = response:getMessage()
break
elseif response and response:isDone() then
break
elseif response and response:isInvalidAccount() then
self.found_accounts[response:isInvalidAccount()] = true
end
end
local timediff = (os.time() - interval_start)
-- This thread made another guess
thread_data.guesses = (thread_data.guesses and thread_data.guesses + 1 or 1)
-- Dump statistics at regular intervals
if timediff > Engine.STAT_INTERVAL then
interval_start = os.time()
local tps = self.counter / (os.time() - self.starttime)
table.insert(self.tps, tps)
stdnse.debug2("threads=%d,tps=%.1f", self:activeThreads(), tps)
end
-- if delay was specified, do sleep
if self.options.delay > 0 then
stdnse.sleep(self.options.delay)
end
condvar "signal"
end
condvar "signal"
end,
--- Adds new worker thread using start function
--
-- @return new thread object
addWorker = function (self, cvar)
local co = stdnse.new_thread(self.login, self, cvar)
Engine.THREAD_TO_ENGINE[co] = self
self.threads[co] = {
running = true,
protocol_error = nil,
attempt = 0,
in_batch = false,
ready = false,
connection_error = nil,
con_error_reason = nil,
username = nil,
password = nil,
}
return co
end,
addWorkerN = function (self, cvar, n)
assert(n >= 0)
for i = 1, n do
self:addWorker(cvar)
end
end,
renewBatch = function (self)
if self.batch then
local size = self.batch:getSize()
local data = self.batch:getData()
for i = 1, size do
if self.threads[data[i]] then
self.threads[data[i]].in_batch = false
self.threads[data[i]].ready = false
end
end
end
self.batch = Batch:new(math.min(self:threadCount(), 3), self.tick)
end,
readyBatch = function (self)
if not self.batch then
return false
end
local n = self.batch:getSize()
local data = self.batch:getData()
if n == 0 then
return false
end
for i = 1, n do
if self.threads[data[i]] and coroutine.status(data[i]) ~= "dead" and self.threads[data[i]].in_batch then
if not self.threads[data[i]].ready then
return false
end
end
end
return true
end,
--- Starts the brute-force
--
-- @return status true on success, false on failure
-- @return err string containing error message on failure
start = function (self)
local cvar = {}
local condvar = nmap.condvar(cvar)
assert(self.options.script_name, "SCRIPT_NAME was not set in options.script_name")
assert(self.port.number and self.port.protocol, "Invalid port table detected")
self.port.service = self.port.service or "unknown"
-- Only run the check method if it exist. We should phase this out
-- in favor of a check in the action function of the script
if self.driver:new(self.host, self.port, self.driver_options).check then
-- check if the driver is ready!
local status, response = self.driver:new(self.host, self.port, self.driver_options):check()
if not status then
return false, response
end
end
local usernames = self.usernames
local passwords = self.passwords
if "function" ~= type(usernames) then
return false, ("Invalid usernames iterator: %s"):format(usernames)
end
if "function" ~= type(passwords) then
return false, ("Invalid passwords iterator: %s"):format(passwords)
end
local mode = self.options.mode or stdnse.get_script_args "brute.mode"
-- if no mode was given, but a credfile is present, assume creds mode
if not mode and stdnse.get_script_args "brute.credfile" then
if stdnse.get_script_args "userdb" or stdnse.get_script_args "passdb" then
return false, "\n ERROR: brute.credfile can't be used in combination with userdb/passdb"
end
mode = 'creds'
end
-- Are we guessing against a service that has no username (eg. VNC)
if self.options.passonly then
local function single_user_iter (next)
local function next_user ()
coroutine.yield ""
end
return coroutine.wrap(next_user)
end
-- only add this iterator if no other iterator was specified
if self.iterator == nil then
self.iterator = Iterators.user_pw_iterator(single_user_iter(), passwords)
end
elseif mode == 'creds' then
local credfile = stdnse.get_script_args "brute.credfile"
if not credfile then
return false, "No credential file specified (see brute.credfile)"
end
local f = io.open(credfile, "r")
if not f then
return false, ("Failed to open credfile (%s)"):format(credfile)
end
self.iterator = Iterators.credential_iterator(f)
elseif mode and mode == 'user' then
self.iterator = self.iterator or Iterators.user_pw_iterator(usernames, passwords)
elseif mode and mode == 'pass' then
self.iterator = self.iterator or Iterators.pw_user_iterator(usernames, passwords)
elseif mode then
return false, ("Unsupported mode: %s"):format(mode)
-- Default to the pw_user_iterator in case no iterator was specified
elseif self.iterator == nil then
self.iterator = Iterators.pw_user_iterator(usernames, passwords)
end
if (not mode or mode == 'user' or mode == 'pass') and self.options.useraspass then
-- if we're only guessing passwords, this doesn't make sense
if not self.options.passonly then
self.iterator = unpwdb.concat_iterators(
Iterators.pw_same_as_user_iterator(usernames, "lower"),
self.iterator
)
end
end
if (not mode or mode == 'user' or mode == 'pass') and self.options.emptypass then
local function empty_pass_iter ()
local function next_pass ()
coroutine.yield ""
end
return coroutine.wrap(next_pass)
end
self.iterator = Iterators.account_iterator(usernames, empty_pass_iter(), mode or "pass")
end
self.starttime = os.time()
-- How many threads should start?
local start_threads = self.start_threads
-- If there are already too many threads waiting for connection,
-- then start humbly with one thread
if nmap.socket.get_stats().connect_waiting > 0 then
start_threads = 1
end
-- Start `start_threads` number of threads
self:addWorkerN(cvar, start_threads)
self:renewBatch()
local revive = false
local killed_one = false
local error_since_batch_start = false
local stagnation_count = 0 -- number of times when all threads are stopped because of exceptions
local quick_start = true
local stagnated = true
-- Main logic loop
while true do
local thread_count = self:threadCount()
-- should we stop
if thread_count <= 0 then
if (self.initial_accounts_exhausted and #self.retry_accounts == 0) or self.terminate_all then
break
else
-- there are some accounts yet to be checked, so revive the engine
revive = true
end
end
-- Reset flags
killed_one = false
error_since_batch_start = false
-- Are all the threads have any kind of mistake?
-- if not, then this variable will change to false after next loop
stagnated = true
-- Run through all coroutines and check their statuses
-- if any mistake has happened kill one coroutine.
-- We do not actually kill a coroutine right-away, we just
-- signal it to finish work until some point an then die.
for co, v in pairs(self.threads) do
if not v.connection_error then
stagnated = false
end
if v.protocol_error or v.connection_error then
if v.attempt >= self.batch:getStartTime() then
error_since_batch_start = true
end
if not killed_one then
v.terminate = true
killed_one = true
if v.protocol_error then
stdnse.debug2("Killed one thread because of PROTOCOL exception")
else
stdnse.debug2("Killed one thread because of CONNECTION exception")
end
end
-- Remove error flags of the thread to let it continue to run
v.protocol_error = nil
v.connection_error = nil
else
-- If we got here, then at least one thread is running fine
-- and there is no connection stagnation
--stagnated = false
end
end
if stagnated == true then
stagnation_count = stagnation_count + 1
-- If we get inside `if` below, then we are not making any
-- guesses for too long. In this case it is reasonable to stop
-- bruteforce.
if stagnation_count == 100 then
stdnse.debug1("WARNING: The service seems to have failed or is heavily firewalled... Consider aborting.")
if self.options.killstagnated then
self.error = "The service seems to have failed or is heavily firewalled..."
self.terminate_all = true
end
stagnation_count = 0
end
else
stagnation_count = 0
end
-- `quick_start` changes to false only once since Engine starts
-- `quick_start` remains false till the end of the bruteforce.
if killed_one then
quick_start = false
end
-- Check if we possibly exhaust resources.
if not killed_one then
local waiting = nmap.socket.get_stats().connect_waiting
if waiting ~= 0 then
local kill_count = 1
if waiting > 5 then
kill_count = math.max(math.floor(thread_count / 2), 1)
end
for co, v in pairs(self.threads) do
if coroutine.status(co) ~= "dead" then
stdnse.debug2("Killed one because of RESOURCE management")
v.terminate = true
killed_one = true
kill_count = kill_count - 1
if kill_count == 0 then
break
end
end
end
end
end
-- Renew the batch if there was an error since we started to assemble the batch
-- or the batch's limit is unreachable with current number of threads
-- or when some thread does not change state to ready for too long
if error_since_batch_start
or not killed_one and thread_count < self.batch:getLimit()
or (thread_count > 0 and self.tick - self.batch:getStartTime() > 10) then
self:renewBatch()
end
if (not killed_one and self.batch:isFull() and thread_count < self.max_threads)
or revive then
local num_to_add = 1
if quick_start then
num_to_add = math.min(self.max_threads - thread_count, thread_count)
end
self:addWorkerN(cvar, num_to_add)
self:renewBatch()
revive = false
end
local threads = self:threadCount()
stdnse.debug2("Status: #threads = %d, #retry_accounts = %d, initial_accounts_exhausted = %s, waiting = %d",
threads, #self.retry_accounts, tostring(self.initial_accounts_exhausted),
nmap.socket.get_stats().connect_waiting)
if threads > 0 then
-- wake up other threads
-- wait for all threads to finish running
condvar "broadcast"
condvar "wait"
end
end
local valid_accounts
if not self.options.nostore then
valid_accounts = creds.Credentials:new(self.options.script_name, self.host, self.port):getTable()
else
valid_accounts = self.credstore
end
local result = stdnse.output_table()
-- Did we find any accounts, if so, do formatting
if valid_accounts and #valid_accounts > 0 then
result[self.options.title or "Accounts"] = valid_accounts
else
result.Accounts = "No valid accounts found"
end
-- calculate the average tps
local sum = 0
for _, v in ipairs(self.tps) do
sum = sum + v
end
local time_diff = (os.time() - self.starttime)
time_diff = (time_diff == 0) and 1 or time_diff
local tps = (sum == 0) and (self.counter / time_diff) or (sum / #self.tps)
-- Add the statistics to the result
result.Statistics = ("Performed %d guesses in %d seconds, average tps: %.1f"):format( self.counter, time_diff, tps )
if self.options.max_guesses > 0 then
-- we only display a warning if the guesses are equal to max_guesses
for user, guesses in pairs(self.account_guesses) do
if guesses == self.options.max_guesses then
result.Information = ("Guesses restricted to %d tries per account to avoid lockout"):format(self.options.max_guesses)
break
end
end
end
-- Did any error occur? If so add this to the result.
if self.error then
result.ERROR = self.error
return false, result
end
return true, result
end,
getEngine = function (co)
local engine = Engine.THREAD_TO_ENGINE[co]
if not engine then
stdnse.debug1("WARNING: No engine associated with %s", coroutine.running())
end
return engine
end,
getThreadData = function (co)
local engine = Engine.getEngine(co)
if not engine then
return nil
end
return engine.threads[co]
end,
}
--- Default username iterator that uses unpwdb
--
function usernames_iterator ()
local status, usernames = unpwdb.usernames()
if not status then
return usernames or "Failed to load usernames"
end
return usernames
end
--- Default password iterator that uses unpwdb
--
function passwords_iterator ()
local status, passwords = unpwdb.passwords()
if not status then
return passwords or "Failed to load passwords"
end
return passwords
end
Iterators = {
--- Iterates over each user and password
--
-- @param users table/function containing list of users
-- @param pass table/function containing list of passwords
-- @param mode string, should be either 'user' or 'pass' and controls
-- whether the users or passwords are in the 'outer' loop
-- @return function iterator
account_iterator = function (users, pass, mode)
local function next_credential ()
local outer, inner
if "table" == type(users) then
users = unpwdb.table_iterator(users)
end
if "table" == type(pass) then
pass = unpwdb.table_iterator(pass)
end
if mode == 'pass' then
outer, inner = pass, users
elseif mode == 'user' then
outer, inner = users, pass
else
return
end
for o in outer do
for i in inner do
if mode == 'pass' then
coroutine.yield(i, o)
else
coroutine.yield(o, i)
end
end
inner "reset"
end
while true do
coroutine.yield(nil, nil)
end
end
return coroutine.wrap(next_credential)
end,
--- Try each password for each user (user in outer loop)
--
-- @param users table/function containing list of users
-- @param pass table/function containing list of passwords
-- @return function iterator
user_pw_iterator = function (users, pass)
return Iterators.account_iterator(users, pass, "user")
end,
--- Try each user for each password (password in outer loop)
--
-- @param users table/function containing list of users
-- @param pass table/function containing list of passwords
-- @return function iterator
pw_user_iterator = function (users, pass)
return Iterators.account_iterator(users, pass, "pass")
end,
--- An iterator that returns the username as password
--
-- @param users function returning the next user
-- @param case string [optional] 'upper' or 'lower', specifies if user
-- and password pairs should be case converted.
-- @return function iterator
pw_same_as_user_iterator = function (users, case)
local function next_credential ()
for user in users do
if case == 'upper' then
coroutine.yield(user, user:upper())
elseif case == 'lower' then
coroutine.yield(user, user:lower())
else
coroutine.yield(user, user)
end
end
users "reset"
while true do
coroutine.yield(nil, nil)
end
end
return coroutine.wrap(next_credential)
end,
--- An iterator that returns the username and uppercase password
--
-- @param users table containing list of users
-- @param pass table containing list of passwords
-- @param mode string, should be either 'user' or 'pass' and controls
-- whether the users or passwords are in the 'outer' loop
-- @return function iterator
pw_ucase_iterator = function (users, passwords, mode)
local function next_credential ()
for user, pass in Iterators.account_iterator(users, passwords, mode) do
coroutine.yield(user, pass:upper())
end
while true do
coroutine.yield(nil, nil)
end
end
return coroutine.wrap(next_credential)
end,
--- Credential iterator (for default or known user/pass combinations)
--
-- @param f file handle to file containing credentials separated by '/'
-- @return function iterator
credential_iterator = function (f)
local function next_credential ()
local c = {}
for line in f:lines() do
if not (line:match "^#!comment:") then
local trim = function (s)
return s:match '^()%s*$' and '' or s:match '^%s*(.*%S)'
end
line = trim(line)
local user, pass = line:match "^([^%/]*)%/(.*)$"
coroutine.yield(user, pass)
end
end
f:close()
while true do
coroutine.yield(nil, nil)
end
end
return coroutine.wrap(next_credential)
end,
unpwdb_iterator = function (mode)
local status, users, passwords
status, users = unpwdb.usernames()
if not status then
return
end
status, passwords = unpwdb.passwords()
if not status then
return
end
return Iterators.account_iterator(users, passwords, mode)
end,
}
-- These functions all return a boolean and an error (or result)
-- and should all be wrapped in order to check status of the engine.
checkwrap = {
connect = true,
send = true,
receive = true,
receive_lines = true,
receive_buf = true,
receive_bytes = true,
}
-- A socket wrapper class.
-- Instances of this class can be treated as regular sockets.
-- This wrapper is used to relay connection errors to the corresponding Engine
-- instance.
BruteSocket = {
new = function (self)
local o = {
socket = nil,
}
setmetatable(o, self)
self.__index = function (instance, key)
local f = rawget(self, key)
if f then
-- BruteSocket function
return f
else
-- something provided by NSE socket
f = instance.socket[key]
end
-- Check if it should be wrapped with a checkStatus call
if checkwrap[key] then
return function(s, ...)
local status, err = f(instance.socket, ...)
instance:checkStatus(status, err)
return status, err
end
elseif type(f) == "function" then
-- not wrapped? call the function on the underlying socket
return function (s, ...)
return f(instance.socket, ...)
end
end
return f
end
o.socket = nmap.new_socket()
return o
end,
getSocket = function (self)
return self.socket
end,
checkStatus = function (self, status, err)
if not status and (err == "ERROR" or err == "TIMEOUT") then
local engine = Engine.getEngine(coroutine.running())
if not engine then
stdnse.debug2("WARNING: No associated engine detected for %s", coroutine.running())
return -- behave like a usual socket
end
local thread_data = Engine.getThreadData(coroutine.running())
engine.retry_accounts[#engine.retry_accounts + 1] = {
username = thread_data.username,
password = thread_data.password,
}
thread_data.connection_error = true
thread_data.con_error_reason = err
end
end,
}
function new_socket ()
return BruteSocket:new()
end
return _ENV
|